(juejin.cn/post/705288… "juejin.cn/post/705288…
一、前情回顾 & 背景
上一篇篇小作文开始介绍 patch 函数
,重点讨论的是进行初次渲染的过程,我们把 createPatchFunction
和它返回的 patch 函数
进行了简化,只留下能够表达初次渲染过程的代码,具体如下:
其中 patch 函数会调用 createElm
方法将 vnode
节点树变成真实 DOM
树并插入到 body
。createElm
中会创建原生的 HTML
元素和自定义组件
。
因涉及了自定义组件的渲染是一个大篇幅的工作,本篇就展开聊一聊自定义组件的渲染过程。
注意:这个自定义组件的渲染不是独立的,他是初次渲染的一个分支流程。为什么这么说?以这个模板为例子:
<div id="app"><some-com /></div>
,初次渲染从id
为app
的div
虚拟节点渲染,当渲染他的children
时就会渲染some-com
这个自定义组件,这时createElem
就开始处理自定义组件了。所以根实例的初次渲染和组件的初次渲染不冲突。
二、createElm
方法位置:src/core/vdom/patch.js -> function createPatchFunction 内部方法
方法参数:
vnode
:虚拟节点实例对象,在初次渲染时就是前面调用vm._render()
获取到的整棵虚拟 DOM 树insertedVnodeQueue
,待插入的节点队列parentElm
,父节点,当把vnode
变成真实dom
后插入到这个父节点中refElm
:参照物元素,是 div#app 的兄弟节点,如果有值,要把 vnode 渲染出来的DOM插入到它的前面nested
,是否嵌套owernArray
,所有者数组index
,索引
方法作用:创建原生 HTML
元素和自定义组件;
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested
// 重点来啦:
// 这个 createComponent 负责处理 vnode 是自定义组件的情况
// 如果是 vnode 是一个普通元素,createComponent 调用后返回 false
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
// 如果 vnode 是自定义组件,createComponent 执行后返回 true,到这里就终止了
return
}
// 能走到这里说明 vnode 是个普通的元素
// 获取 data 对象
const data = vnode.data
// 获取子节点列表
const children = vnode.children
// vnode 的标签名
const tag = vnode.tag
if (isDef(tag)) {
// 创建新节点,并挂载到 vnode 对象上,
// vnode.elm 是个真实的 DOM 元素
vnode.elm = vnode.ns // ns 是命名空间,忽略他
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode) // 咱们研究这种情况
if (__WEEX__) {
} else {
// 递归创建所有子节点(普通元素,组件)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 调用 createHooks
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 初次渲染时将节点插入父节点,这是至关重要的一步了,
// vnode.elm 是创建出来的真实元素,到了这里包含所有模板内容的一整棵 DOM 树,
// parentElm 是 body 元素
// 把 DOM 元素插入到 body,实现渲染
insert(parentElm, vnode.elm, refElm)
}
} else if (isTrue(vnode.isComment)) {
// vnode.tag 属性不存在,即不是元素或者自定义组件
// 到这里就是注释节点,创建注释节点并插入父节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 不是注释、也不是元素,就当文本处理了
// 文本节点,创建文本节点并插入父节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
复制代码
2.1 createComponent
方法位置:src/core/vdom/patch.js -> function createPatchFunction 的内部方法
方法参数:
vnode
:节点对象insertedVnodeQueue
,待插入节点列表parentElm
, 父元素refElm
,ref
参照元素
方法作用:
- 如果
vnode
是一个组件,则执行init
钩子,创建组件实例并挂载 - 然后为组件执行各个模块的
create
钩子 - 如果组件被
keep-alive
包裹,则激活组件 - 如果是自定义组件返回
true
,如果是普通HTML元素什么也不处理返回undefined
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// 获取 vnode.data 对象
let i = vnode.data
if (isDef(i)) {
// 判断组件实例是否已经存在 && 被 <keep-alive/> 包裹
// 被 keep-alive 包裹的组件是激活和失活,普通组件需要新建和销毁
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 执行 vnode.data.hook.init 钩子,
// 这个东西是生成 vnode 时,通过 installComponentHooks
// 如果是被 keep-alive 包裹的组件:
// 则执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
// 如果是组件未被 keep-alive 包裹或首次渲染,则初始化组件,并进入挂载阶段
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 当调用过 data.hook.init 钩子后,如果这个 vnode 是个子组件,
// 此时就应该已经生成过子组件实例并完成挂载了
// 给子组件设置 vnode.elm
if (isDef(vnode.componentInstance)) {
// 如果一个 vnode 是一个子组件,
// 则调用 data.hook.init 钩子之后会创建一个组件实例,并实施挂载
// 这个时候就可以给组件执行各个模块的 create 钩子
initComponent(vnode, insertedVnodeQueue)
// 将组件的 DOM 节点插入到父节点内
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
// 组件被 keep-alive 包裹的情况,激活组件
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
复制代码
2.1.1 data.hook.init
data
就是 parse
阶段解析模板中元素的行内属性、指令所得的对象,在创建自定义组件的 VNode
时会在 VNode.data
上增加 hook
属性,其中包含了四个钩子方法:init/prepatch/insert/destroy
,这个过程大致过程的代码示例如下:
export function createComponent () {
installComponentHooks(data);
}
// installComponentHooks
function installComponentHooks (data: VNodeData) {
// data.hook 对象初始化
const hooks = data.hook || (data.hook = {})
// 遍历 hooksToMerge 数组,
// hooksToMerge = Object.keys(componentVnodeHooks)
// hooksToMerge = ['init', 'prepatch', 'insert', 'destroy']
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
复制代码
从上面的代码可以看出这些钩子方法应该在 componentVnodeHooks
对象里面,其核心工作如下:
- 处理
vnode.componentInstance
已经存在并且组件处于keep-alive
包裹,则直接调用hook.prepatch
进入patch
,因为keep-alive
的组件没有销毁,不需要再重新创建组件实例了,直接走patch
的更新渲染; - 另一种场景就是需要创建组件实例并且挂载到
vnode.componentInstance
上。创建组件实例时通过new 组件的构造函数
,而这个构造函数
则是在createComponent
时通过组件的选项对象
和Vue.options
合并,然后通继承Vue
得到的子类:function VueComponent
; - 得到实例以后,手动调用子组件的
$mount
方法,使子组件进入挂载阶段;这就有趣了,子组件$mount
就会接着走子组件模板的编译(parse+generate
)得到子组件
的render 函数
,然后创建子组件
的渲染 watcher
,得到VNode
,然后调用子组件._update()
... 你会发现这是个递归的过程,如果子组件还有子组件,就接着进入这个循环过程,直到所有的组件都被挂载到对应的父节点。
const componentVNodeHooks = {
// 初始化
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance && // 组件实例已存在
!vnode.componentInstance._isDestroyed && // 组件实例未被销毁
vnode.data.keepAlive // 组件处于 keep-alive 的包裹中
) {
// 被 keep-alive 包裹的组件,其 init 的过程走 prepatch
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 普通组件的创建过程:
// 执行过这里,就得到了 vnode.componentInstance 即组件实例,
// 也就是通过组件构造函数创建出来的实例
// 这个实例的构造函数是扩展 Vue 得到的子类,继承了 Vue 的能力
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 执行组件的 $mount 方法手动挂载
// 进入挂载阶段,接下来就是通过编译器得到 render 函数,
// 然后创建渲染 watcher 接着走组件 mount、patch
// 这条路直到组件被挂载到父节点上
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// 更新 VNode,用新的 VNode 配置更新就的 VNode
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
},
// 执行组件的 mounted 生命周期钩子
insert (vnode: MountedComponentVNode) {
},
// 销毁组件:
destroy (vnode: MountedComponentVNode) {
}
}
复制代码
2.1.2 data.hook.prepatch
prepatch
更多的是表达响应式数据发生变化后进行 diff + patch
的这一渲染过程,暂时先不展开
const componentVNodeHooks = {
// 初始化
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
// 更新 VNode,用新的 VNode 配置更新旧的 VNode
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// 新的 VNode,用新的 VNode 配置更新就的 VNode 上的各种属性
const options = vnode.componentOptions
// 老的 VNode 组件的组件实例
const child = vnode.componentInstance = oldVnode.componentInstance
// 用 vnode 上的属性更新 child 上的各种属性
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert (vnode: MountedComponentVNode) {},
destroy (vnode: MountedComponentVNode) {}
}
复制代码
2.1.3 createComponentInstanceForVnode
方法位置:src/core/vdom/create-component.js -> function createComponentInstanceForVnode
方法参数:
vnode
, 虚拟节点parent
,父元素
方法作用:给
export function createComponentInstanceForVnode (
vnode: any,
parent: any // 当前处于激活状态的父实例,比如 <some-com /> 的父实例就是根实例
): Component {
const options: InternalComponentOptions = {
_isComponent: true, // 标识当前实例是个组件
_parentVnode: vnode, // 当前组件的父节点是 vnode
parent
}
// 检查内联模板的渲染函数,如果是内联模板,
// 则需要把 render 函数替换成内联模板的渲染函数
// 内联模板,看 vue 官方文档吧
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// vnode.componentOptions.Ctor 就是生成 vnode 的时候扩展 Vue 所得到的子类
// 每个自定义组件都有自己的子类构造函数
return new vnode.componentOptions.Ctor(options)
}
复制代码
2.1.4 initComponent
初始化组件,主要工作在 invokeCreateHooks
方法中,值得一提的是这个 create
并不是组件的 created
生命周期钩子,而是属性、样式、类名、指令、ref
(attrs/stle/klass/directives/ref
)的周期方法,这些方法在执行 createPatchFunction({ nodeOps, modules })
时传入的 modules
选项中;
在 Vue 的官方文档中有介绍指令的周期函数,这写周期函数就是这里说的 hook
;
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 初次渲染走这里,调用 modules 中的各个模块的 create 钩子方法;
// 注意这个不是组件的 created 生命周期,而是 attr、klass(类名)、style、directives 属性的
// 维护工作,这里有个例子,是关于指令 directives 的钩子函数:
// https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
复制代码
2.2 nodeOps.createElement
举个例子,nodeOps
中封装了浏览器的 DOM API
,createElement
就是创建元素的方法,创建的是真实的 DOM
元素;
// 创建标签名为 tagName 的元素节点
export function createElement (tagName: string, vnode: VNode): Element {
// 创建元素节点
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// 如果 select 元素,则为他设置 multiple 属性
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
复制代码
2.3 createChildren
方法位置:src/core/vdom/patch.js -> function createPatchFunction 的内部方法
方法参数:
vnode
,虚拟节点列表children
,vnode.children
列表,即当前虚拟节点的子节点列表;insertedVnodeQueue
,待插入的节点列表
方法作用:根据 vnode.children
递归调用 createElm
方法创建元素,并插入到父元素(vnode.elm
)中;
// 创建所有子节点,并将子节点插入到父节点,形成一颗 DOM 树
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
// children 是数组,标识一组节点
if (process.env.NODE_ENV !== 'production') {
// 检测这一组节点的 key 是否重复
checkDuplicateKeys(children)
}
// 遍历子节点列表,递归调用 createElm 创建这些节点,
// 然后插入父节点,形成一棵 DOM 树
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 文本节点,创建文本节点并插入父节点
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
复制代码
2.4 invokeCreateHooks
调用各个 module
的 create
方法,比如attrs
,style
,directives
,然后执行组件的 mounted
生命周期方法;这些 module
是 createPatchFunction({ nodeOps, modules })
时传入的 modules
选项;
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
// 调用组件的 data.hook.insert 钩子,执行组件的 mounted 生命周期方法
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
复制代码
i.insert
是 VNode.data.hook.insert
钩子,是在创建 VNode
时,通过 installComponentHooks
在 data.hook
2.4.1 data.hook.insert
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
// 更新 VNode,用新的 VNode 配置更新就的 VNode
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
// 执行组件的 mounted 生命周期钩子
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
// 如果组件未挂载,则调用组件的 mounted 生命周期钩子
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// 处理 keep-alive 组件的异常情况....
},
// 销毁组件:
destroy (vnode: MountedComponentVNode) {}
}
复制代码
2.5 insert
方法位置:src/core/vdom/patch.js -> function createPatchFunction 内部方法
方法参数:
parent
,父节点elm
,待插入到父节点队列的元素ref
,参照物节点,是elm
弟弟节点,确保插入后节点的顺序
方法作用:将 elm
插入到 parent
子节点队列;执行这一步骤后,VNode
就变成真实 DOM
了。
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
// 插入到 ref 参照物节点的前面,所以 ref 是 elm 的弟弟节点
nodeOps.insertBefore(parent, elm, ref)
}
} else {
// 追加到 parent 的子节点末尾
nodeOps.appendChild(parent, elm)
}
}
}
复制代码
三、总结
3.1 本文总结
本文详细讨论了 createElm
这个方法的逻辑,这个方法根据 VNode
创建真实的元素,其中包含两种场景:
-
如果
VNode
是自定义组件,则调用createComponent
方法处理,它内部会调用data.hook.init
相当于Vue.prototype._init
方法,完成子组件的实例化,然后调用子组件的$mount
开始进行子组件的编译以及挂载过程,完成子组件的渲染;在进行子组件的渲染过程中,就会触发patch
函数关于子组件的初渲染逻辑; -
如果是普通元素,则通过
nodeOps.createElement
创建原生HTML
元素,并处理其子节点的过程,而处理子节点则又是一个递归调用createElm
方法的过程; -
在
createElm
方法的结尾处会把得到的vnode.elm
也就是根据vnode
得到的真实 DOM
插入到parentElm
,这里的parentElm
就是body
元素,如下图;
3.2 挂载阶段性总结
后面伴随着移除占位符节点等一系列工作,执行栈逐步推出,直至 Vue.prototype.$mount
执行栈推出,至此 Vue
的初次渲染结束。
以下为从根实例 new Vue
一直到 VNode
插入到 body
的调用栈梳理,执行栈推出,从底出栈,至此 $mount
的初次挂载阶段同步结束;
// 调用栈梳理,最上层为栈顶
new Vue()
-> Vue.prototype._init()
-> Vue.prototype.$mount()
-> compileToFunctions(template...) 编译模板获取 render 函数
-> mount()
-> mountComponent()
-> new Watcher(updateComponent)
-> updateComponent
-> Vue.prototype._render()
-> Vue.prototype._update()
-> Vue.prototype.__patch__()
-> createPatchFunction 返回值 patch()
-> createElm()
-> insert(parentElm, vnode.elm)
复制代码