写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】Component – 源码版 之 挂载组件 DOM
由这篇文章 从模板到 DOM 的简要流程
我们知道,在生成 VNode 之后,下一步就是根据 VNode 生成 DOM 然后挂载了
在本文开始之前你可以先看 Component – 白话版 先整体了解下 component
现在开始我们的正文
上一篇文章 Component – 创建组件 VNode,我们已经说到了【页面模板解析成 VNode 树】的步骤
那今天就就到了【页面 VNode 生成 DOM 挂载】了
等等,今天说的不是 Component 挂载 DOM 吗?跟页面 Vnode 有什么关系??是啊,component 的挂载肯定是跟着父页面的啊,你自己挂?自挂东南枝吗?
好了,废话不说,马上开始
前言预告
这篇 从模板到 DOM 的简要流程 已经说过下面的步骤
1vm._render 执行得到 页面 VNode
2vm._update 拿到 页面 VNode,会开始 patch,不断比对【旧 VNode 和 刚拿到的新 VNode】
对比完之后,会调用一个 createElm 的方法去创建 DOM,然后插入页面
那现在,我们就从 createElm 这个方法突破,前面的流程跟本内容无关,一律略过
function createElm(vnode, parentElm, refElm) {
// 组件需要特殊处理
if (createComponent(vnode, parentElm, refElm)) return
... 正常的标签,需要不断递归子节点调用 createElm,然后生成 DOM,并插入到父节点
}
createElm 的作用就是根据 标签名创建 DOM 节点,然后挂载到父节点中,其中参数如下
parentElm == 父 DOM 节点
refElm == 兄弟 DOM 节点,你插入父节点,可能也要知道插在谁附近不是吗,不能乱插的
然后很明显,createElm 每次掉要给你都会调用【createComponent】去检测这个标签是否是组件
如果是组件,就会去创建这个组件的实例,并且 返回 true,从而不用去执行 createElm 下面的部分
调用组件生命钩子
看下 createComponent
function createComponent(vnode, parentElm, refElm) {
var data = vnode.data;
var hook = i.hook;
var init = i.init;
// 调用子组件的 init 方法, init 方法就是 Vue.prototype._init
if (init) {
// 创建子组件的 vm 实例
init(vnode, parentElm, refElm);
// 如果存在组件实例,就是上一步创建成功了
if (vnode.componentInstance) {return true}
}
}
有没有好奇 vnode.data.hook.init 是什么吗?
他是每个组件,都会被【注册进外壳节点的钩子函数】,没错,就是下面的钩子,源码
什么是组件生命钩子
没错,这就是那个钩子的源码
var componentVNodeHooks = {init(vnode, parentElm, refElm) {
var vm=
vnode.componentInstance =
createComponentInstanceForVnode(
vnode,activeInstance,
parentElm, refElm
);
// 因为 在 Vue.prototype._init 中,只有 $options 存在 el,才会挂载 dom
// 这里手动挂载组件
vm.$mount(vnode.elm);
}
...
}
那么,钩子是什么时候注册的呢?
嗯,在上一篇文章,【创建组件外壳 VNode 的过程中】,然后保存到了外壳节点的 data 上
function createComponent(
Ctor, data, context,
children, tag
) {
... 创建组件构造函数
var hooks = data.hook || (data.hook = {});
data.hook.init = componentVNodeHooks.init
... 创建组件 VNode,并保存组件构造函数 和钩子 等到 vnode 中
}
打印一下实际 VNode,没错,有很多钩子,但是现在只说 init
来吧,仔细看那个 init 钩子源码,你可以看到调用了一个方法
createComponentInstanceForVnode
开始深入探索它 ………
创建组件实例
createComponentInstanceForVnode 函数作用就是给 component【增加定制 options】+【调用组件构造函数】
function createComponentInstanceForVnode(
vnode, parent,
parentElm, refElm
) {
// 增加 component 特有 options
var options = {
_isComponent: true,
parent: parent, // 父实例
_parentVnode: vnode, // 外壳节点
_parentElm: parentElm , // 父 DOM
_refElm: refElm // 兄弟 DOM
};
// vnode.components.Ctor 就是 构造函数,里面会调用 Vue.prototype._init
return new vnode.componentOptions.Ctor(options)
}
vnode.componentOptions.Ctor 就是 构造函数,就是下面这个,上篇文章 Component – 创建组件 VNode 时保存在外壳节点的
function VueComponent(options) {this._init(options);
}
new 了之后,自然而然,走到了 _init 方法,在 init 方法中,有一个特殊照顾 component 的方法,专门给 component 实例设置 options
“ 这一步跟 挂载组件 DOM 没什么关联,想去掉的,但是想想还是先保留下来,完整整个流程 ”
Vue.prototype._init = function(options) {if (如果是组件) {initInternalComponent(vm, options);
}
}
组件初始化 initInteralComponent
function initInternalComponent(vm, options) {
// 这个 options 就是在创建构造函数时,合并的 options,全局选项和组件设置选项
var opts = vm.$options = Object.create(vm.constructor.options);
// 保存父节点,外壳节点,兄弟节点等
var parentVnode = options._parentVnode; // _parentVnode 是外壳节点
opts.parent = options.parent; // options.parent 是 父实例
opts._parentVnode = parentVnode;
opts._parentElm = options._parentElm;
opts._refElm = options._refElm;
// 保存父组件给子组件关联的数据
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
// 保存渲染函数
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
这个时候,init 的过程就完成了
下一步就是到了 mount 过程
组件解析模板并挂载
可以再回看下「componentVNodeHooks.init」那个钩子源码
在创建组件实例成功之后,会手动调用实例 vm.$mount 进行挂载,就是这句代码完成的功能
然而,挂载的步骤,就是正常标签挂载的步骤了
详情可以查看 从模板到 DOM 的简要流程
的 mount 过程,是一毛一样的,就不多说了
总结
1、父页面已经拿到了 VNode,其中会调用 createElm 根据 VNode 生成 DOM,进行挂载
2、不断的递归遍历子节点,使用 createComponent 判断标签是否是组件
3、遇到组件,拿到组件外壳 VNode 的 data(data 保存有父组件给子组件的,事件,props,构造函数,钩子)
4、从 data 中拿到 hook,hook 中拿到 init 钩子,并执行 init 钩子
5、init 钩子中,调用 createComponentInstanceForVnode 调用组件构造函数,并返回组件
6、init 钩子中,使用上一步返回的实例,手动调用 vm.$mount 进行组件内部模板解析渲染,并挂载