Skip to content

十一、组件原理

0、使用

  • 全局组件
js
Vue.component('my-button', {
	template:'<button>+</button>'
});
  • 局部组件
js
let vm = new Vue({
	el: '#app',
	components:{
		aa:{
			template:'<div>hello </div>'
		}
	},
	data: { },
});

1、组件的作用

  • 实现复用
  • 方便维护
  • 合理拆分组件可以提高性能:因为每个组件都有一个Watcher,当组件更新的时候,越小的组件,vdom越小,就能减少比对,提高性能

2、组件初始化

  • 通过Vue.component注册全局组件,之后可以在模板中进行使用
  • Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上
  • Vue.extend方法就是创建出一个子类,继承于Vue,并返回这个类
js
// src\global-api\index.js

export function initGlobalApi(Vue) {
    Vue.options = {};
    Vue.mixin = function(mixin) {
        // 合并对象-生命周期
        this.options = mergeOptions(this.options, mixin);
	}

	// 核心就是创造一个子类继承我们的父类
	let cid = 0;
	Vue.extend = function(extendOptions) {
		const Super = this; // this > vue的构造函数Vue
		// 定义子类的构造函数
		const Sub = function VueComponent(options) {
			// 子类实例的初始化方法
			this._init(options);
		}
		// 唯一标识
		Sub.cid = cid++;
		// 子类要继承父类原型上的方法, 原型继承
		Sub.prototype = Object.create(Super.prototype);
		Sub.prototype.constructor = Sub;
		// 合并其他属性
		Sub.options = mergeOptions(Super.options, extendOptions);
		Sub.components = Super.components;
		// 返回子类
		return Sub;
	}
	
	// 全局组件方法
	Vue.options._base = Vue; // 保留Vue的构造函数
	Vue.options.components = {};
	Vue.component = function(id, definition) {
		// 默认以name属性为准
		definition.name = definition.name || id;
		// 根据当前组件对象 生成了一个子类的构造函数,用于指向父类,用的时候得new definition().$mount()
		definition = this.options._base.extend(definition);
		// 缓存到options中
		Vue.options.components[id] = definition;
	}
}
  • 因为在初始化_init方法中会合并属性,需要加一个合并策略
js
// src\util.js
// 组件的合并策略-就近策略:当同时存在全局组件和局部组件的时候,以局部组件为主,没有再用全局组件
strats.components = function(parentVal, childVal) {
	// 将全局组件放到原型链上,沿着原型链进行查找
	const res = Object.create(parentVal);
	if (childVal) {
		for(let key in childVal){
            res[key] = childVal[key];
        }
	}
	return res;
}

2、组件转虚拟dom

  • vdom中的_c方法中调用createElement创建元素方法中进去区分组件还是原生标签
  • 给组件的vdom标记属性,存放构造函数和插槽
js
// src\vdom\index.js

// 生成元素节点的虚拟dom对象
function createElement(vm, tag, data = {}, ...children) {
	// 判断是否为原生标签
	if (isReservedTag(tag)) {
		// 原生标签直接创建虚拟节点
		return vnode(tag, data, data.key, children);
	} else {
		// 如果是组件,在产生虚拟节点时需要把组件的构造函数传入 new Ctor().$mount()
		let Ctor = vm.$options.components[tag];
		return createComponent(vm, tag, data, data.key, children, Ctor);
	}
}

// 生成组件
function createComponent(vm, tag, data, key, children, Ctor) {
	const baseCtor = vm.$options._base;
	// 如果组件是一个对象,需要通过Vue.extend来创建一个子组件构造函数
	if (typeof Ctor === 'object') {
		Ctor = baseCtor.extend(Ctor);
	}
	// 给子组件增加生命周期
	data.hook = {
		// 组件初始化会调用init方法,然后挂载
		init(vnode) {
			let child = vnode.componentInstance = new Ctor({});
			child.$mount();
		}
	}
	return vnode(`vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, 
	{ Ctor, children });
}

// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text, componentOptions) {
    return {
        tag,
        data,
        key,
        children,
		text,
		// 组件的虚拟节点多一个属性,用来保存当前组件的构造函数和他的插槽
		componentOptions
    }
}

3、组件转真实dom

js
export function patch(oldVnode, vnode) {
	// 组件初始化的时候 oldvnode为undefined
	if(!oldVnode){ // 如果是组件这个oldVnode是个undefined
        return createElm(vnode); // vnode是组件中的内容
	}
	// ...
}

function createComponent(vnode) {
	// 调用hook中init方法 
	let i = vnode.data;
	// 拿到hook中的init方法,然后调用,内部会new 子组件,然后挂载到vnode上
	if ((i = i.hook) && (i = i.init)) {
		i(vnode);
	}
	if (vnode.componentInstance) {
		return true;
	}
}

export function createElm(vnode) {
    let { tag, children, key, data, text } = vnode;
    if (typeof tag == 'string') {

		// 如果是组件,组件渲染后的结果 放到当前组件的实例上 vm.$el
		if (createComponent(vnode)) {
			// 返回组件对应的dom元素
			return vnode.componentInstance.$el;
		}

        // 创建元素,放到vnode.el上
        vnode.el = document.createElement(tag);

        // 只有元素才有属性
        updateProperties(vnode);

        // 遍历儿子,将儿子渲染后的结果放到父亲中
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 创建文本,放到vnode.el上
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

4、组件的渲染流程

  1. 调用Vue.component,注册全局组件
  2. 内部用的是Vue.extend 就是产生一个子类来继承父类
  3. 等会创建子类实例时会调用父类的_init方法,再$mount
  4. 组件的初始化就是 new 这个组件的构造函数并且调用$mount方法
  5. 创建虚拟节点 根据标签筛选出对应的组件,然后生成组件的虚拟节点 componentOptions里面包含Ctor,children
  6. 组件创建真实dom时 (先渲染的是父组件) 遇到是组件的虚拟节点时,去调用init方法,让组件初始化并挂载, 组件的mountdomvm.el上 =》 vnode.componentInstance中,这样渲染时就 获取这个对象的$el属性来渲染

Released under the MIT License.