我们在上篇文章分析了虚拟节点的创建及渲染流程,其中也有简单分析了 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