一,前言
上篇,介绍了组件部分-组件的编译,主要涉及以下几部分:
- 组件编译流程介绍:
html->render->vnode
- 创建组件虚拟节点:
createComponent
本篇,组件部分-组件的生命周期;
二,前文回顾
Vue.component
方法:
Vue.component
方法用于注册 Vue 全局组件;- 当组件定义 definition 为对象时,在
Vue.component
内部会使用Vue.extend
方法对组件定义 definition 进行处理,返回组件的构造函数; - 将组件与组件的构造函数关系维护到全局对象
Vue.options.components
中,便于后续的使用;
- 当 new Vue 初始化时:
- new Vue 会执行 Vue 原型方法 _init 进行初始化流程,mergeOptions 会进行组件的合并;
- 通过
mergeOptions(vm.constructor.options, options)
将全局组件定义合并到局部组件定义上; - 组件查找时,会优先查找对应的局部组件定义,若未找到再通过链
__proto__
继续向上查找全局组件定义;
- 组件的编译流程:
- 与模板编译流程一致,html 模板->AST语法树->生成 render 函数,执行_c(即 createElement)创建虚拟节点;
- 在 createElement 方法中,如果是组件则通过 createComponent 方法创建组件虚拟节点componentVnode;
- 在 createComponent 方法中,当 Ctor 为对象时,使用 Vue.extend 进行处理,生成组件的构造函数;
- componentVnode 中包含 componentOptions 组件选项,componentOptions 中包含组件构造函数 Ctor(此时 componentOptions 中的 Ctor 一定为函数);
到这里,就完成了组件虚拟节点的创建;
注意:所有的组件都是通过 Vue.extend 方法来实现的;
接下来,根据组件的虚拟节点创建组件的真实节点,之后再进行挂载;
todo 又读了一遍,感觉写的还不好, 先做标记:
1,全局组件注册:包含 Vue.component 和 extend 两部分
...
后续仍需改善
三,组件的生命周期
当组件初始化时,执行初始化的回调,即hook钩子;在不同的地方执行不同的钩子
new Ctor()
扩展组件 data 属性,为组件添加生命周期钩子函数:
/**
* 创造组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// 获取 Vue 并通过 Vue.extend 将对象处理成为组件的构造函数
Ctor = vm.$options._base.extend(Ctor)
}
// 扩展组件的生命周期
data.hook = {
init(){
let child = new Ctor({});
child.$mount();
},
prepatch(){},
postpatch(){}
}
// 创建vnode时,组件是没有文本的,需要传入 undefined
// 注意:组件没有孩子,组件的孩子就是插槽,将 children 放到组件的选项中
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
4,创建组件的真实节点
如何去找组件的真实节点?
create 方法完成之后,会继续执行 patch 方法,
createElm 方法,是真正将虚拟节点转化为真实节点的位置;
由于组件的加入,此时的 createElm 方法中可能会包含 componentOptions:
第一次:id 为 app 的真实节点
第二次:my-button 组件
在原 createElm 方法中,是直接通过document.createElement(tag)
创建标签,现在还需要添加对组件类型的处理,通过createComponent
创建组件的真实节点:
/**
* 创造组件的真实节点
* @param {*} vnode
*/
function createComponent(vnode) {
console.log(vnode)
}
// 面试:虚拟节点的实现?如何将虚拟节点渲染成真实节点
export function createElm(vnode) {
// 虚拟节点必备的三个:标签,数据,孩子
let { tag, data, children, text, vm } = vnode;
debugger;
// vnode.el:绑定真实节点与虚拟节点的映射关系,便于后续的节点更新操作
if (typeof tag === 'string') { // 元素
// 组件的处理
if(createComponent(vnode)){ // 将组件的虚拟节点,创建成为组件的真实节点
}
// 处理当前元素节点
vnode.el = document.createElement(tag) // 创建元素的真实节点
updateProperties(vnode, data) // 处理元素的 data 属性
// 处理当前元素节点的儿子:递归创建儿子的真实节点,并添加到对应的父亲中
children.forEach(child => { // 若不存在儿子,children为空数组
vnode.el.appendChild(createElm(child))
});
} else { // 文本:文本中 tag 是 undefined
vnode.el = document.createTextNode(text) // 创建文本的真实节点
}
return vnode.el;
}
在 createComponent 方法中,在 data 属性上进行组件生命周期的扩展:
// src/vdom/index.js
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// 获取 Vue 并通过 Vue.extend 将对象处理成为组件的构造函数
Ctor = vm.$options._base.extend(Ctor)
}
// 扩展组件的生命周期
data.hook = {
init(){},
prepatch(){},
postpatch(){}
}
// 创建vnode时,组件是没有文本的,需要传入 undefined
// 注意:组件没有孩子,组件的孩子就是插槽,将 children 放到组件的选项中
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
在 createComponent 方法中,尝试获取生命周期 hook,如果 hook 存在说明是组件,再拿到 init 方法,处理虚拟节点 vnode:
/**
* 创造组件的真实节点
* @param {*} vnode
*/
function createComponent(vnode) {
console.log(vnode);
let i = vnode.data;
if((i = i.hook)&&(i = i.init)){ // 最后 i 为 init 方法
i(vnode); // 将 vnode 传入 init 方法
}
}
先把 hook 赋值给 i,再把 init 赋值给 i,
最终 i 为 init 方法;
组件的初始化,即 new 组件的构造函数,此时就会进入 init 方法:
todo
4,创建组件的真实节点
通过 new Ctor 拿到组件的实例,并调用组件的 $mount
方法,生成一个 $el
;
vnode.componentInstance = new Ctor()
vnode.componentInstance.$el
即为组件渲染后的结果;
5,将组件的 vnode.componentInstance.$el
插入到父标签中
6,当组件实例化 new Ctor() 时,会进行组件的初始化,此时会为组件添加一个独立的渲染过程,为每个组件生成各自的渲染 watcher;当组件更新时,只需要更新自己组件对应的渲染 watcher 即可;所以性能是非常高的,因为在组件渲染时,组件对应的属性会收集自己的渲染 watcher
四,结尾
本篇,介绍了组件部分-组件的生命周期,主要涉及以下几部分:
todo
下一篇,组件部分-生成组件的真实节点
今天公司值班没有网络,先怼上明天周末再修改,见谅~
备注:今天更新了一小部分,这篇还有太多问题,明天继续搞定,见谅~
维护日志
- 20210814:
- 优化了“前文回顾”部分,总结前置知识点,能够更平滑过渡到当前内容;