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