在组件化章节
,我们介绍了 Vue 的组件化实现过程,不过我们只讲了 Vue 组件的创建过程,并没有涉及到组件数据发生变化,更新组件的过程。而通过我们这一章对数据响应式原理
的分析,了解到当数据发生变化的时候,会触发渲染 watcher
的回调函数
,进而执行组件的更新
过程,接下来我们来详细分析这一过程。
1 | updateComponent = () => { |
组件的更新还是调用了 vm._update
方法,我们再回顾一下这个方法,它的定义在 src/core/instance/lifecycle.js
中:
1 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { |
组件更新的过程,会执行 vm.$el = vm.__patch__(prevVnode, vnode)
,它仍然会调用 patch 函数
,在 src/core/vdom/patch.js
中定义:
1 | return function patch (oldVnode, vnode, hydrating, removeOnly) { |
这里执行 patch
的逻辑和首次渲染是不一样的,因为 oldVnode
不为空,并且它和 vnode
都是 VNode
类型,接下来会通过 sameVNode(oldVnode, vnode)
判断它们是否是相同的 VNode
来决定走不同的更新逻辑:
1 | function sameVnode (a, b) { |
sameVnode
的逻辑非常简单,如果两个 vnode
的 key
不相等,则是不同的;
否则继续判断对于同步组件,则判断 isComment
、data
、input
类型等是否相同,对于异步组件,则判断 asyncFactory
是否相同。
所以根据新旧 vnode
是否为 sameVnode
,会走到不同的更新逻辑,我们先来说一下不同的情况。
新旧节点不同
如果新旧 vnode
不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步:
创建新节点
1 | const oldElm = oldVnode.elm |
以当前旧节点
为参考节点,创建新的节点
,并插入到 DOM 中
,createElm
的逻辑我们之前分析过。
更新父的占位符节点
1 | // update parent placeholder node element, recursively |
我们只关注主要逻辑即可,找到当前 vnode
的父的占位符节点
,先执行各个 module
的 destroy
的钩子函数,如果当前占位符
是一个可挂载的节点
,则执行 module
的 create
钩子函数。对于这些钩子函数的作用,在之后的章节会详细介绍。
删除旧节点
1 | // destroy old node |
把 oldVnode
从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes
方法:
1 | function removeVnodes (parentElm, vnodes, startIdx, endIdx) { |
- 遍历待删除的
vnodes
做删除,其中removeAndInvokeRemoveHook
的作用是从DOM
中移除节点并执行module
的remove
钩子函数,并对它的子节点递归
调用removeAndInvokeRemoveHook
函数; invokeDestroyHook
是执行module
的destory
钩子函数以及vnode
的destory
钩子函数,并对它的子vnode
递归调用invokeDestroyHook
函数;removeNode
就是调用平台的DOM API
去把真正的DOM
节点移除。
在之前介绍组件生命周期的时候提到 beforeDestroy
& destroyed
这两个生命周期钩子函数,它们就是在执行 invokeDestroyHook
过程中,执行了 vnode
的 destory
钩子函数,它的定义在 src/core/vdom/create-component.js
中:
1 | const componentVNodeHooks = { |
当组件并不是 keepAlive
的时候,会执行 componentInstance.$destroy()
方法,然后就会执行 beforeDestroy
& destroyed
两个钩子函数。
新旧节点相同
对于新旧节点不同的情况,这种创建新节点
-> 更新占位符节点
-> 删除旧节点
的逻辑是很容易理解的。
还有一种组件 vnode
的更新情况是新旧节点相同
,它会调用 patchVNode
方法,它的定义在 src/core/vdom/patch.js
中:
1 | function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { |
patchVnode
的作用就是把新的 vnode
patch
到旧的 vnode
上,这里我们只关注关键的核心逻辑,我把它拆成四步骤
:
执行 prepatch
钩子函数
1 | let i |
当更新的 vnode
是一个组件 vnode
的时候,会执行 prepatch
的方法,它的定义在 src/core/vdom/create-component.js
中:
1 | const componentVNodeHooks = { |
prepatch
方法就是拿到新的vnode
的组件配置
以及组件实例
,去执行 updateChildComponent
方法,它的定义在 src/core/instance/lifecycle.js
中:
1 | function updateChildComponent( |
由于更新了 vnode
,那么 vnode
对应的实例 vm
的一系列属性也会发生变化,包括占位符 vm.$vnode
的更新、slot
的更新,listeners
的更新,props
的更新等等。
执行 update 钩子函数
1 | if (isDef(data) && isPatchable(vnode)) { |
回到 patchVNode
函数,在执行完新的 vnode
的 prepatch
钩子函数,会执行所有 module
的 update
钩子函数以及用户自定义 update
钩子函数,对于 module
的钩子函数,之后我们会有具体的章节针对一些具体的 case 分析。
完成 patch 过程
如果 vnode
是个文本节点
且新旧文本不相同
,则直接替换文本内容
。如果不是文本节点,则判断它们的子节点
,并分了几种情况处理:
oldCh
与ch
都存在且不相同时,使用updateChildren
函数来更新子节点
,这个后面重点讲。- 如果只有
ch
存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过addVnodes
将ch
批量插入到新节点elm
下。 - 如果只有
oldCh
存在,表示更新的是空节点
,则需要将旧的节点通过removeVnodes
全部清除 - 当只有旧节点是文本节点的时候,则清除其节点文本内容。
执行 postpatch 钩子函数
1 | if (isDef(data)) { |
再执行完 patch
过程后,会执行 postpatch
钩子函数,它是组件自定义的钩子函数,有则执行。
那么在整个 pathVnode
过程中,最复杂的就是 updateChildren 方法了
总结
组件更新的过程核心就是新旧 vnode diff
,对新旧节点相同以及不同的情况分别做不同的处理。
新旧节点不同的更新流程是创建新节点
->更新父占位符节点
->删除旧节点
;
而新旧节点相同的更新流程是去获取它们的 children
,根据不同情况做不同的更新逻辑。
最复杂的情况是新旧节点相同
且它们都存在子节点
,那么会执行 updateChildren
逻辑