一,前言
上篇,介绍了组件部分-组件的生命周期,主要涉及以下几部分:
本篇,组件部分-生成组件的真实节点;
二,生成组件的真实节点
1,前文回顾
前篇,在 createElement
方法中,扩展了对组件的处理:createComponent
方法:生成组件的虚拟节点;
按照模板的渲染流程,接下来会进入 patch
方法,其中的 createElm
方法:将虚拟节点转化成为真实节点;
// todo 后续需详细描述相关流程,对 patch 和 createElm 方法进行必要的介绍和说明;
2,createElm 方法
在 patch
方法中,createElm
方法会将虚拟节点生成为真实节点:
通过vnode.el = document.createElement(tag)
直接创建出真实节点
export function createElm(vnode) {
// vnode.el:绑定真实节点与虚拟节点的映射关系,便于后续的节点更新操作
// 虚拟节点必备的三个:标签,数据,孩子
let { tag, data, children, text, vm } = vnode;
if (typeof tag === 'string') { // 处理元素节点
// 创建真实节点
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;
}
现在,由于组件的加入,createElm
方法中可能会存在 componentOptions
:
打印 createElm
查看:
第一次:真实节点:id=app
第二次:组件:my-button
添加对组件的处理
/**
* 创造组件的真实节点
* @param {*} vnode
*/
function createComponent(vnode) {
console.log(vnode) // my-button
}
export function createElm(vnode) {
let { tag, data, children, text, vm } = vnode;
if (typeof tag === 'string') {// 元素 or 组件
// 添加对组件的处理
if(createComponent(vnode)){ // 将组件的虚拟节点,创建成为组件的真实节点
}
// 创建真实节点
vnode.el = document.createElement(tag)
updateProperties(vnode, data)
children.forEach(child => {
vnode.el.appendChild(createElm(child))
});
} else { // 文本
vnode.el = document.createTextNode(text)
}
return vnode.el;
}
3,组件的初始化 Hook
之前在组件的 data
属性上,扩展出了生命周期 hook
;
在 createComponent
中获取 hook
,如果有 hook
说明就是组件;
拿到 hook
中的 init
方法,并使用 init
方法处理 vnode
:
/**
* 创造组件的真实节点
* @param {*} vnode
*/
function createComponent(vnode) {
console.log(vnode);
let i = vnode.data;
// 先确定有 hook;再拿到 init 方法;
if((i = i.hook)&&(i = i.init)){ // 最后 i 为 init 方法
i(vnode); // 使用 init 方法处理 vnode
}
}
备注:
- 先将 hook 赋值给 i:看是否有 hook,如果有 hook 就是组件;
- 再将 hook 中的 init 方法赋值给 i;
- 最终 i 就是 hook 上的init 方法;
使用 hook
上的 init
方法处理 vnode
,在 hook
中进行组件的初始化:
/**
* 创造组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
Ctor = vm.$options._base.extend(Ctor)
}
data.hook = {
init(){
// 创建组件的实例并挂载
let child = new Ctor({});
child.$mount();
},
prepatch(){},
postpatch(){}
}
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
在 new Ctor
时,会执行 _init
进行组件的初始化:
// 调用子类的初始化 _init 方法
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
initState(vm);
// 由于 el 不存在,所以不会执行 vm.$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
通过 child.$mount()
进行挂载,但没有传参,此时 el = null
,所以不会执行挂载:
Vue.prototype.$mount = function (el) {
const vm = this;
const opts = vm.$options;
el = document.querySelector(el); // 获取真实的元素
vm.$el = el;// $el:页面上的真实元素
if (!opts.render) {
let template = opts.template;
if (!template) {
template = el.outerHTML;
}
let render = compileToFunction(template);
opts.render = render;
}
mountComponent(vm);
}
如果组件的 render
函数不存在,使用组件的 template
编译为 render
函数并保存起来,之后 mountComponent
进行组件的挂载:
export function mountComponent(vm) {
let updateComponent = ()=>{
vm._update(vm._render());
}
callHook(vm, 'beforeCreate');
// 生成渲染 watcher :每个组件都具有独立的渲染 watcher
new Watcher(vm, updateComponent, ()=>{
callHook(vm, 'created');
},true)
callHook(vm, 'mounted');
}
updateComponent
会调用 _render
方法,根据子组件创造虚拟节点:
Vue.prototype._render = function () {
const vm = this;
let { render } = vm.$options;
let vnode = render.call(vm);
return vnode
}
通过 render.call
产生虚拟节点,返回的 vnode
就是模板的 button
;
todo 这一节,应该是生成虚拟节点,需要重构;
三,结尾
本篇,介绍了组件部分-生成组件的真实节点;
下篇,组件部分 - 组件挂载流程简述;
维护记录
- 20230228:
- 调整了文章目录结构;
- 添加 todo:重新梳理规划组件部分知识点,重新进行拆分和优化;