highlight: tomorrow-night-eighties
theme: cyanosis
前言
Vue 的 patch 过程分为上下两篇
通过这篇文章可以了解如下内容
- 父子组件的DOM树是如何建立关联的
- mounted 生命周期的执行顺序
第一次渲染
当创建Render Watcher
时会执行updateComponent
函数 javascript updateComponent = () => { vm._update(vm._render(), hydrating) }
updateComponent
执行vm._render
函数,获取 VNode,然后执行vm._update
javascript Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el // dom 节点 // 获取老的 VNode const prevVnode = vm._vnode // 设置 activeInstance 并返回一个匿名函数,匿名函数返回值是上一个 activeInstance 的值 const restoreActiveInstance = setActiveInstance(vm) // 当前 Vue 实例的 render 函数创建的 VNode vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // 设置 vm.$el // 首次渲染时走这里 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新页面时走这里 vm.$el = vm.__patch__(prevVnode, vnode) } // 将 activeInstance 的值设置成上一个 vm 实例 restoreActiveInstance() if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } }
vm._update
函数先将变量activeInstance
设置成当前的组件实例,然后执行vm.__patch__
函数;
vm.__patch__
函数的作用是创建、渲染、返回节点,并将vm.__patch__
的返回值赋值给vm.$el
。vm.__patch__
执行完成后将变量activeInstance
设置成上一个组件实例,如果此时是根组件则为 null
接下来就重点介绍vm.__patch__
,函数定义在/src/platforms/web/runtime/index.js
javascript Vue.prototype.__patch__ = inBrowser ? patch : noop
而patch
函数定义在src/platforms/web/runtime/patch.js
javascript export const patch: Function = createPatchFunction({ nodeOps, modules })
运用函数柯里化的方式,将平台特有API在这里区分,而不是在调用时通过if...else
判断。
对于modules
其实就是平台特有的一些操作,比如:attr
、class
、style
、event
等,还有核心的 directive
和 ref
,它们会向外暴露一些特有的方法
比如 directive
,向外抛出了create
、update
、destroy
javascript export default { create: updateDirectives, update: updateDirectives, : function unbindDirectives (vnode: VNodeWithData) { updateDirectives(vnode, emptyNode) } }
createPatchFunction
接下来会执行createPatchFunction
函数,函数定义在src/core/vdom/patch.js
中,createPatchFunction
函数内部定义了很多辅助方法,这些方法就暂时不一一列举了,等后面边用边看,先看下主要逻辑 ```javascript const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend // 将 modules 中导出的值都放到 cbs 中 for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } }
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {}
} ``
createPatchFunction函数最终的返回值是
patch方法,也就是说
vm.patch实际执行的是这里返回的
patch方法,而在这之前会从
modules中找到相应的方法,添加到
cbs,最后
cbs`的数据结构如下
bash cbs = { create: [fn1, fn2, ...], update: [fn1, fn2, ...], ... }
cbs
中的这些方法在 patch 不同阶段调用,做相应的操作,比如创建指令等;cbs
初始化完成之后会调用patch
方法
```javascript return function patch (oldVnode, vnode, hydrating, removeOnly) { // 新节点不存在,老节点存在,销毁老节点 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
let isInitialPatch = false
const insertedVnodeQueue = []
// VNode是组件的渲染VNode,并且是第一次渲染
if (isUndef(oldVnode)) {
// 新节点存在,老节点不存在(首次渲染组件时会出现这种情况)
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断 oldVnode 是否为真实节点
const isRealElement = isDef(oldVnode.nodeType)
// oldVnode 不是真实元素并且 oldVnode 和 VNode 是同一个节点,则执行 patchVnode
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// oldVnode 是真实节点
if (isRealElement) {
// 根据 oldVnode(此时 oldVnode 是真实节点) 创建一个 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 获取节点的 真实元素
const oldElm = oldVnode.elm
// 获取 oldVnode 的 父节点
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 更新 组件vnode 的 elm 并重新执行父组件的 cbs.create 和 insert hooks(不包含 mounted 钩子)
if (isDef(vnode.parent)) {
// ...
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
} ```
首先 patch
函数会判断新节点是否存在,如果新节点不存在,老节点存在,调用 destroy
,销毁老节点;反之,判断是否存在老节点:
- 如果不存在则调用
createElm
方法去创建并插入节点,比如组件的 渲染VNode 初次渲染时,会走这个逻辑。 - 如果存在,判断老节点是否为真实节点;
- 如果不是真实节点并且新旧节点相同,说明是更新过程,调用
patchVnode
函数; - 如果上述判断没有成立,继续判断老节点是不是真实节点,如果是,根据
oldVnode
创建一个VNode对象并赋值给oldVnode
。然后获取oldVnode
的真实节点和父节点 (如果oldVnode
为null
,则获取的真实节点和父节点都为null
); - 接下来调用
createElm
方法去创建并插入节点,这里的createElm
方法会传入父节点和oldVnode
的相邻节点;比如根实例设置了el
属性、或者更新过程中新旧节点不同都会走这个逻辑。等节点创建插入完成之后 删除老节点
- 如果不是真实节点并且新旧节点相同,说明是更新过程,调用
patch
函数的主要作用就是调用createElm
创建并返回真实DOM
createElm
```javascript function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // 获取 data 对象 const data = vnode.data // 获取 children const children = vnode.children // 节点标签 const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { // ... } // 创建 dom 节点 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode)
if (__WEEX__) {
// ...
} else {
// 递归创建所有子节点,生成整颗 dom 树
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else if (isTrue(vnode.isComment)) {
// 创建注释节点,并插入父节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 创建文本节点,并插入父节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
} ```
createElm
基于 VNode 创建整棵 DOM 树,并插入到父节点上。
createElm
中会先调用createComponent
方法,判断传入VNode的类型,如果传入的是组件占位符VNode,则执行创建组件的逻辑,返回true
;如果传入的是普通VNode,返回false
,通过createChildren
创建子元素DOM;整个组件的DOM树创建完成之后将其插入传入的父节点中。
这里分别说一下普通标签和组件标签的 patch 过程
普通标签DOM创建过程
首先 createElm
会调用createComponent
方法,因为是一个普通VNode,返回false
。接下来获取data
、tag
、children
;然后判断tag
是否为空:
- 如果
tag
为空,并且vnode.isComment
为true
,说明是一个注释VNode,创建注释节点,插入到父节点中 - 如果
tag
为空,并且vnode.isComment
为false
,说明是一个文本VNode,创建文本节点,插入到父节点中 - 如果
tag
不为空,调用nodeOps.createElement
创建节点,并赋值给vnode.elm
,然后调用createChildren
创建子节点,将子节点插入到vnode.elm
;子节点插入完成后将vnode.elm
插入到父节点
createChildren
javascript function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { // 检测这组节点的 key 是否重复 checkDuplicateKeys(children) } 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))) } }
如果children
是一个数组, 遍历children
,调用createElm
依次创建这些节点并插入父节点中。如果不是一个数组,并且是一个文本VNode的话,创建一个文本节点并插入到父节点
组件标签DOM创建过程
如果当前的VNode是组件占位符VNode,在 createElm
中调用createComponent
方法,会执行创建组件的过程,返回true
先看下createComponent
方法
createComponent
javascript function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // 判断组件实例是否已经存在, 并且组件被 keep-alive 包裹 const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { // 执行 组件的 init 钩子函数 i(vnode, false /* hydrating */) } if (isDef(vnode.componentInstance)) { // 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性 initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
在创建组件占位符VNode时,给vnode.data
添加了几个钩子函数,其中就包含init
钩子,所以createComponent
会先判断vnode.data
上有没有init
钩子函数如果有则执行这个钩子函数,看下init
的代码
javascript const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // ... } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {}, insert (vnode: MountedComponentVNode) {}, destroy (vnode: MountedComponentVNode) {} }
init
方法的if
是针对于keep-alive
的,这个后期会说,现在就看else
的逻辑就行,else
里面会执行createComponentInstanceForVnode
方法,并传入vnode
、activeInstance
(当前vm实例)。
javascript export function createComponentInstanceForVnode ( vnode: any, parent: any, // activeInstance in lifecycle state ): Component { // 给组件vue实例的 options 添加 _isComponent、_parentVnode、parent 属性 const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, // 组件占位符 VNode parent } const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) }
createComponentInstanceForVnode
方法首先会定义一个options
对象,并添加属性
_isComponent
:是否为组件_parentVnode
:组件的占位符VNodeparent
:父组件实例
然后执行return new vnode.componentOptions.Ctor(options)
,在创建组件占位符VNode时,通过Vue.extend
方法创建了一个子组件的构造函数,并将他存到了vnode.componentOptions.Ctor
里面,所以这里会为子组件创建并返回一个Vue
实例。也就是说他会执行一系列的初始化操作等
回到init
方法,将createComponentInstanceForVnode
方法返回的Vue
实例赋值给vnode.componentInstance
,也就是说 组件占位符VNode 的componentInstance
属性指向子组件实例,创建了子组件实例后会执行child.$mount(hydrating ? vnode.elm : undefined, hydrating)
,调用 $mount
方法挂载子组件,在这个过程中会创建子组件的Render Watcher
然后执行子组件的render
函数创建组件的渲染VNode,并做依赖收集,接着执行_update
函数、__patch__
函数,就和上面的流程一样了。
javascript vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
当子组件的挂载过程执行完成后,子组件Vue
实例的$el
属性就是子组件的 DOM树;回到createComponent
方法,执行initComponent
函数,initComponent
函数内会执行vnode.elm = vnode.componentInstance.$el
,将子组件的DOM树赋值给组件占位符VNode 的elm
属性,然后执行insert(parentElm, vnode.elm, refElm)
,将DOM树插入父元素中,并返回true
,接着回到createElm
中,因为createComponent
返回true
,所以不会继续向下执行。initComponent
方法不是只有这一点逻辑,还会执行cbs
中的钩子函数,这些逻辑在其他章节会说。
总结
父子组件的DOM树是如何建立关联的
当组件创建Render Watcher
时,执行render
函数获取组件的渲染VNode;然后执行_update
函数,_update
函数内执行patch
函数创建节点并插入到DOM中;如果组件中有子组件,调用组件占位符VNode的init
钩子函数,为子组件创建Vue
实例,执行子组件的$mount
方法创建Render Watcher
,并对子组件执行上述流程;等子组件执行完成之后将子组件的DOM树挂载到组件占位符VNode的elm
上,并将其插入到父元素中或相邻元素前后。这样父子组件的DOM树就关联起来了。
大体流程图如下
mounted生命周期是如何执行的
假设有3个组件分别是,根组件、A
组件、B
组件,他们的关系是 根组件是A
的父组件,A
是B
的父组件
在执行根组件的 patch 过程中,会执行A
组件的$mount
方法,进而根组件 patch 过程停滞,先执行A
的 patch 过程,B
组件也是这个流程,A
组件的 patch 过程停滞,执行B
的 patch 过程,执行patch
函数中有这样一段逻辑 javascript return function patch (oldVnode, vnode, hydrating, removeOnly) { let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else {} invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
创建一个isInitialPatch
变量和一个insertedVnodeQueue
数组,根据组件的渲染VNode创建DOM树时,传入的oldVnode
为null
,会将isInitialPatch
设置成true
,渲染VNode的DOM树创建完成后,会执行invokeInsertHook
函数 javascript function invokeInsertHook (vnode, queue, initial) { if (isTrue(initial) && isDef(vnode.parent)) { // 每个组件的渲染 VNode 才会执行,vnode.parent 指向 组件VNode vnode.parent.data.pendingInsert = queue } else { // 执行 insertedVnodeQueue 中所有 VNode 的 insert hook for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } }
如果当前VNode是组件的渲染 VNode 并且没有老节点,invokeInsertHook
会将insertedVnodeQueue
添加到vnode.parent.data.pendingInsert
里面,也就是组件占位符VNode的data.pendingInsert
中。此时这个insertedVnodeQueue
为空数组,因为B
组件中没有子组件;到此B
组件的DOM树已经创建完成,回到createComponent
方法,继续执行A
组件的 patch 过程。
createComponent
函数会执行initComponent
javascript function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } // 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性 vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } else { registerRef(vnode) insertedVnodeQueue.push(vnode) } }
此时的vnode
参数是B
组件的占位符VNode,将vnode.data.pendingInsert
添加到insertedVnodeQueue
里面;然后给B
组件的占位符VNode设置elm
属性;判断B
组件是不是一个空组件,不管是不是都会执行insertedVnodeQueue.push(vnode)
;也就是说将B
组件的占位符VNode添加到insertedVnodeQueue
里面。执行到patch
函数时,这里和B
组件一样isInitialPatch
也是true
,所以执行invokeInsertHook
时,会将insertedVnodeQueue
赋值给A
组件占位符VNode的data.pendingInsert
。
A
组件的 patch 过程结束,回到根组件的 patch 过程中,继续执行createComponent
,createComponent
内执行initComponent
;将A
组件占位符VNode的data.pendingInsert
添加到insertedVnodeQueue
里面;此时insertedVnodeQueue
有两个元素分别是B
组件的占位符VNode和A
组件的占位符VNode;当根组件的patch
函数执行invokeInsertHook
时,就会走else
逻辑,因为根组件的patch
函数传入的oldVnode
是有值的(真实节点,比如#app
),所以isInitialPatch
为false
,此时invokeInsertHook
会执行insertedVnodeQueue
中所有VNode的insert
钩子函数;
```javascript const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean {}, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance.isMounted) { componentInstance.isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) {} }, destroy (vnode: MountedComponentVNode) {} } `` 在创建组件的占位符VNode时,会挂载一个
insert钩子函数。这个钩子函数会给组件实例设置
_isMounted为
true,代表已经挂载完成,然后执行组件的
mounted`生命周期
当根组件的 patch 过程结束后,Render Watcher
也创建完成了,回到mountComponent
方法,mountComponent
方法中有这样的逻辑 javascript if (vm.$vnode == null) { // 表示此组件已经挂载完成 vm._isMounted = true // 根组件的 mounted 函数 先子后父 callHook(vm, 'mounted') }
根组件实例的$vnode
指向null
,会执行根组件的mounted
生命周期函数,并将根组件实例的_isMounted
属性设置为true
结论
也就是说在整个 patch 过程中,会将组件占位符VNode收集起来,等 patch 过程结束时,执行所有VNode的insert
钩子函数,顺序是先子后父,其实这里不光会收集组件占位符VNode,还会收集有指令的VNode,前提是指令绑定了insert
钩子。
如果每次在组件 patch 结束后执行当前组件的insert
钩子,由于还没有将组件的DOM树渲染到页面上,所以访问不到DOM节点;只有整个 patch 过程结束后,才能访问到,所以要收集起来统一执行。