1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 全局组件
Vue.component("parent-component", {
template: `<div>我是全局组件</div>`,
});
// Vue实例化
let vm = new Vue({
el: "#app",
data() {
return {
aa: 1,
};
},
// render(h) {
// return h('div',{id:'a'},'hello')
// },
template: `<div id="a">
hello 这是我自己写的Vue{{aa}}
<parent-component><parent-component>
<child-component></child-component>
</div>`,
// 局部组件
components: {
"child-component": {
template: `<div>我是局部组件</div>`,
},
},
});

1.全局组件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
import initExtend from "./initExtend";
import initAssetRegisters from "./assets";
const ASSETS_TYPE = ["component", "directive", "filter"];
export function initGlobalApi(Vue) {
Vue.options = {}; // 全局的组件 指令 过滤器
ASSETS_TYPE.forEach((type) => {
Vue.options[type + "s"] = {};
});
Vue.options._base = Vue; //_base指向Vue

initExtend(Vue); // extend方法定义
initAssetRegisters(Vue); //assets注册方法 包含组件 指令和过滤器
}

initGlobalApi方法主要用来注册Vue的全局方法,类似:Vue.Mixin,Vue.extend,Vue.component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ASSETS_TYPE = ["component", "directive", "filter"];
export default function initAssetRegisters(Vue) {
ASSETS_TYPE.forEach((type) => {
Vue[type] = function (id, definition) {
if (type === "component") {
// this指向Vue
// 全局组件注册
// 子组件可能也有extend方法 VueComponent.component方法
definition = this.options._base.extend(definition);
}
this.options[type + "s"][id] = definition;
};
});
}

通过extend函数把传入的选项处理之后挂载到了Vue.options.components


2.Vue.extend定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}

Vue.extend核心思路是使用原型继承的方法返回了Vue的子类,利用mergeOptions把传入的options和父类的options进行了合并


3.组件的合并策略

处理全局组件和局部组件的合并:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const ASSETS_TYPE = ["component", "directive", "filter"];
// 组件 指令 过滤器的合并策略
function mergeAssets(parentVal, childVal) {
const res = Object.create(parentVal); //比如有同名的全局组件和自己定义的局部组件 那么parentVal代表全局组件 自己定义的组件是childVal 首先会查找自已局部组件有就用自己的 没有就从原型继承全局组件 res.__proto__===parentVal
if (childVal) {
for (let k in childVal) {
res[k] = childVal[k];
}
}
return res;
}

// 定义组件的合并策略
ASSETS_TYPE.forEach((type) => {
strats[type + "s"] = mergeAssets;
});

4.创建组件Vnode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//先判断传入值是否为对象
//通过正则匹配标签名返回标签
import { isObject, isReservedTag } from "../util/index";
// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
let key = data.key;

if (isReservedTag(tag)) {
// 如果是普通标签
return new Vnode(tag, data, key, children);
} else {
// 否则就是组件
let Ctor = vm.$options.components[tag]; //获取组件的构造函数
return createComponent(vm, tag, data, key, children, Ctor);
}
}

function createComponent(vm, tag, data, key, children, Ctor) {
if (isObject(Ctor)) {
// 如果没有被改造成构造函数
Ctor = vm.$options._base.extend(Ctor);
}
// 声明组件自己内部的生命周期
data.hook = {
// 组件创建过程的自身初始化方法
init(vnode) {
let child = (vnode.componentInstance = new Ctor({ _isComponent: true })); //实例化组件
child.$mount(); //因为没有传入el属性 需要手动挂载 为了在组件实例上面增加$el方法可用于生成组件的真实渲染节点
},
};

// 组件vnode 也叫占位符vnode ==> $vnode
return new Vnode(
`vue-component-${Ctor.cid}-${tag}`,
data,
key,
undefined,
undefined,
{
Ctor,
children,
}
);
}

改写createElement方法 对于非普通html标签,就生成组件Vnode,把Ctor和children作为Vnode最后一个参数componentOptions传入


5.渲染组件真实节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// patch用来渲染和更新视图
export function patch(oldVnode, vnode) {
if (!oldVnode) {
// 组件的创建过程是没有el属性的
return createElm(vnode);
} else {
// 非组件创建过程省略
}
}

// 判断是否是组件Vnode
function createComponent(vnode) {
// 初始化组件
// 创建组件实例
let i = vnode.data;
// 下面这句话很关键 调用组件data.hook.init方法进行组件初始化过程 最终组件的vnode.componentInstance.$el就是组件渲染好的真实dom
if ((i = i.hook) && (i = i.init)) {
i(vnode);
}
// 如果组件实例化完毕有componentInstance属性 那证明是组件
if (vnode.componentInstance) {
return true;
}
}

// 虚拟dom转成真实dom
function createElm(vnode) {
const { tag, data, key, children, text } = vnode;
// 判断虚拟dom 是元素节点还是文本节点
if (typeof tag === "string") {
if (createComponent(vnode)) {
// 如果是组件 返回真实组件渲染的真实dom
return vnode.componentInstance.$el;
}
// 虚拟dom的el属性指向真实dom 方便后续更新diff算法操作
vnode.el = document.createElement(tag);
// 解析虚拟dom属性
updateProperties(vnode);
// 如果有子节点就递归插入到父节点里面
children.forEach((child) => {
return vnode.el.appendChild(createElm(child));
});
} else {
// 文本节点
vnode.el = document.createTextNode(text);
}
return vnode.el;
}

如果属于组件Vnode 那么把渲染好的组件真实DOM 指向vnode的组件$el属性返回


6.小结

在构造函数中,initGlobal进行初始化,在全局初始化当中用extend函数结合原型链继承将子类的构造函数返回并init初始化。
通过mergeOptions将组件,指令,过滤器保存在options里面,然后调用extend把生成的组件构造函数挂载到Vue.options.component上,通过合并options实现原型继承返回,创建组件vnode与生命周期,然后调用生命周期并初始化,最后返回vnode.componentInstance.$el渲染完成的真实DOM。