我们在上篇文章分析了虚拟节点的创建及渲染流程,其中也有简单分析了 patch 过程,但是对于新旧节点的对比逻没有去仔细分析,所以我们打算好好梳理下 patch 的整个流程。
PATCH入口
有了上篇文章的基础,我们简单再过过入口就行了,首先在 updateComponent 函数中调用 vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染的入参为真实节点
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新的入参为组件的上次虚拟节点
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
不难找到 __patch__ 的最终调用
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
我们接下来的的重点则是分析 createPatchFunction,并注意此处的传参 nodeOps 及 modules
首次PATCH
我们先来分析首次渲染的情况
export const emptyNode = new VNode('', {}, [])
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 节点数据更新函数添加到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) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 首次渲染将走到这里
if (isRealElement) {
// 创建新的空节点作为oldVnode
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// destroy old node
if (isDef(parentElm)) {
debugger
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
首次渲染的时候,会执行 createElm,将 vnode 作为参数传递,我们看看 createElm 逻辑
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 注意这边如果含有ownerArray参数的时候会执行个浅拷贝的过程
// 我的理解是为了避免vnode在下面的代码中被我们注入其它属性污染
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 组件逻辑暂不分析
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
// 会执行个检查逻辑检查标签的合法性
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 调用createElement创建真实节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 执行节点更新函数
// vnode数据更新节点样式及属性、事件等
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 的逻辑并不复杂,其实就是执行 createElement -> createChildren -> invokeCreateHooks -> insert 的流程。我们再来看看 createChildren
function createChildren (vnode, children, insertedVnodeQueue) {
// 先检查 `key` 是否有重复
// 也是我们经常看到的警告
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
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)))
}
}
createChildren 的主要逻辑就是检查 key 是否有重复,然后递归调用 createElm,最后执行 nodeOps.appendChild 添加节点到父节点。
最后在 patch 中返回 vnode.elm 完成 patch。
更新PATCH
我们再来看看数据更新的时候执行的 patch
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 更新主要执行的是patchVnode逻辑
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
}
}
我们来看看 patchVnode 的代码
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 如果节点数据相同则不同更新
if (oldVnode === vnode) {
return
}
// 浅拷贝
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 赋值elm
const elm = vnode.elm = oldVnode.elm
// ...
// 略过异步组件及静态节点逻辑
let i
const data = vnode.data
// 组件的prepatch hook
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 新旧子节点数组
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 更新节点属性样式等
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 组件hook相关
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 执行子节点对比逻辑
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 1. 新旧子节点数组都存在则调用updateChildren
// updateChildren也就是vue中比较出名的diff算法
// 值得深入研究一番
// 因为以前分析过这边就不再分析了
// 感兴趣可以前往https://juejin.cn/post/6927177789426630663查看
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 2. 只存在新子节点数组
// 判断key属性重复警告
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 将textContent设置为空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 执行addVnodes添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 3. 只存在旧子节点数组
// 直接移除旧子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 4. 无新旧子节点 只存在text
// 将textContent设置为空
nodeOps.setTextContent(elm, '')
}
// 5. 文本节点则直接设置文本即可
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// 触发组件的postpatch钩子略过
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode 的逻辑也很清晰,先调用更新函数 update 更新节点的属性样式等,再通过节点属性及新旧节点的子节点数组对比来更新节点,执行 addVnodes 添加节点或 removeVnodes 删除节点或者是执行 setTextContent 更新节点文本即可。
我们再来看看 addVnodes 和 removeVnodes 的逻辑
addVnodes 比较简单,就是依次调用 createElm 创建新节点并挂载在 parentElm下,parentElm 也就是刚才 patchVnode 中的真实节点
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
}
}
removeVnodes 稍微复杂一点,会编辑节点数据依次执行 removeAndInvokeRemoveHook 和 invokeDestroyHook
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
// 触发removeNode及组件节点的一些操作
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
再继续看看 removeAndInvokeRemoveHook
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
// 略过
} else {
// 我们知道其主要执行removeNode就行
// removeNode则是直接调用removeChild移除节点
removeNode(vnode.elm)
}
}
function removeNode (el) {
const parent = nodeOps.parentNode(el)
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
最后再来看看 invokeDestroyHook
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
// 触发属性更新函数destroy
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
// 触发组件相关的destory钩子
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
// 同时需要对其子节点进行递归调用invokeDestroyHook进行节点移除销毁
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
至此,我们就完成 patch 函数的分析。可以得到 patch 函数主要是通过 vnode 数据来创建真实节点,在更新时通过 patchVnode 来达到 diff 更新。这样就完成了虚拟节点到真实节点的映射。其中 diff 相关的分析大家可以看vueDiff 算法解读
总结
本篇文章分析了 patch 函数的一个执行流程,主要分析首次创建及更新两个过程来分析。因为写的比较匆忙,有些逻辑分析的不是特别清楚,有部分略过的逻辑,如组件的钩子函数执行,后面将在组件化实现中专门对其进行分析。good good staduy day day up