Snabbdom 源码 - patchVnode

patchVnode

patch 方法中对比两个新旧 Vnode 节点时,如果新旧节点是相同节点,调用 patchVnode 方法。在 patchVnode 方法内,分为以下几步:

  1. 触发用户设置的 prepatch 钩子函数(不论新旧节点是否相同,都会触发)
  2. 触发用户设置的 update 钩子函数
  3. 比较相同新旧节点的差异
  4. 触发用户设置的 postpatch 钩子函数,相当于 vueupdated 钩子函数

比较相同新旧节点的差异

开始我们需要声明一些常量:

js 复制代码
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]

老节点是一定有 elm 属性的,对应其真实 DOM 元素,将其赋值给新节点的 elm 和常量 elm(此时新节点的 DOM 其实就是复用了老节点的 DOM)。

接下来就是分析新老节点的各种情况,做出不同的处理,总结来说就是以下四点:

  1. 新老节点都有 children 且不相等,调用 updateChildren
  2. 添加新节点的 childrenaddVnodes
  3. 移除老节点的 childrenremoveVnodes(移除老节点,而不是直接替换,就是为了触发 remove 钩子函数)
  4. 设置或者清除新老节点的 textsetTextContent(仅仅更新了文本的内容,没有重新创建节点)
js 复制代码
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新老节点都有 children 且不相等,调用 updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      } else if (isDef(ch)) {
        // 新节点有 children,老节点没有
        // 如果老节点有 text 属性,清空对应 DOM 元素的 textContent
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 新节点没有 children,老节点有,移除所有的老节点
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 新节点没有 children,也没有 text,老节点没有 children
        api.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新节点有 text,且不等于老节点的 text
      if (isDef(oldCh)) {
        // 如果老节点有 children,移除老节点 children 对应的 DOM 元素
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 设置新节点对应 DOM 元素的 textContent
      api.setTextContent(elm, vnode.text!)
    }

那么在这段比较中,最为复杂的就是当新老节点都有 children 且不相等时,调用的 updateChildren 方法。

updateChildren

js 复制代码
// 新老节点都有 children 且不相等,调用 updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)

updateChildren 中接收了新老节点的子节点,并从中获取了新旧子节点的开始结束节点和他们的索引:

js 复制代码
function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue)

接下来会分情况来进行 Vnode 之间的比较,这个比较也叫做 diff 算法。

diff 算法

diff 算法用于查找两棵树每一个节点的差异。当数据变化后,不直接操作 DOM,而是先比较 js 对象(vnode)是否发生变化,找到所有变化的位置,只最小化的更新变化的位置,从而提高性能。

通常我们会想到遍历 A 树上的所有节点,让其中的每一个节点和 B 树上的每一个节点做对比,这样需要做大量的比较。Snabbdom 根据 DOM 的特点做了一些优化,因为 DOM 操作时很少会跨级别操作节点,所以可以只比较同级别的节点,从而减少比较次数。

sameVnode 比较的就是 keysel 相同。如果2个节点是 sameVnode,会重用之前的旧节点对应的 DOMpatchVnode 会对比差异然后将更新应用到重用的 DOM 元素上,如果文本元素和子元素也相同的话,就无需再操作 DOM

同级别节点比较,直到新子节点或者旧子节点有一个全部遍历完

1. oldCh 的开始节点 == newCh 的开始节点

当比较两个节点的时候,先会比较两个节点的开始节点是否为 sameVnode,如果是则调用 patchVnode 比较差异然后更新到真实 DOM 上,然后将开始节点置为第2个节点接着比较。

js 复制代码
... else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
}

2. oldCh 的结束节点 == newCh 的结束节点

当比较到开始节点不再相等,就比较两个节点的结束节点,,如果是 sameVnode,则调用 patchVnode 比较差异然后更新到真实 DOM 上,然后将开始节点置为倒数第2个节点接着比较。

js 复制代码
else if (sameVnode(oldEndVnode, newEndVnode)) {
   patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
   oldEndVnode = oldCh[--oldEndIdx]
   newEndVnode = newCh[--newEndIdx]
}

3. oldCh 的开始节点 == newCh 的结束节点

比较 oldCh 的开始节点和 newCh 的结束节点,如果是 sameVnode,则调用 patchVnode 比较差异然后更新到真实 DOM 上,并将 oldCh 的开始节点对应的 DOM 元素移动到 parentElm 的最后(不是移动 oldCh,而是移动真实 DOM),然后将 oldCh 的第2个节点置为开始节点,将 newCh 的倒数第2个节点置为结束节点,接着比较。

js 复制代码
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
    api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
}

4. oldCh 的结束节点 == newCh 的开始节点

js 复制代码
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
  oldEndVnode = oldCh[--oldEndIdx]
  newStartVnode = newCh[++newStartIdx]
}

5. oldCh 的开始节点 !== newCh 的任何节点

不是上述4种情况的,遍历 oldCh 中的节点,没有查找到 newCh 的开始节点,说明 newCh 中的开始节点是一个新的节点,此时需要创建新的 DOM 元素,并插入到 oldCh 对应的 DOM 元素的最前面。

6. oldCh 的开始节点 == newCh 的中间某一节点

不是上述4种情况的,遍历 oldCh 中的节点,其中查找到了 newCh 的开始节点,则调用 patchVnode 比较差异然后更新,再将这个节点移动到 oldCh 对应的 DOM 元素的最前面。

js 复制代码
else {
  if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  }
  idxInOld = oldKeyToIdx[newStartVnode.key as string]
  if (isUndef(idxInOld)) {
     api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
   } else {
     elmToMove = oldCh[idxInOld]
     if (elmToMove.sel !== newStartVnode.sel) {
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
     } else {
         patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
      }
    }
    newStartVnode = newCh[++newStartIdx]
}

结束遍历后,处理剩余节点

1. 老节点先遍历完

说明新节点有剩余,把新节点的剩余节点调用 addVnodes 批量插入到 oldCh 对应的 DOM 元素的最右边

2. 新节点先遍历完

说明老节点有剩余,调用 removeVnodes 把老节点的剩余节点批量删除

js 复制代码
if (oldStartIdx > oldEndIdx) {
   before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
   addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
   removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax