Snabbdom 源码 - patchVnode

patchVnode

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

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

比较相同新旧节点的差异

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

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. 添加新节点的 children 用 addVnodes
  3. 移除老节点的 children 用 removeVnodes(移除老节点,而不是直接替换,就是为了触发 remove 钩子函数)
  4. 设置或者清除新老节点的 text 用 setTextContent(仅仅更新了文本的内容,没有重新创建节点)
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 比较的就是 key 和 sel 相同。如果2个节点是 sameVnode,会重用之前的旧节点对应的 DOM,patchVnode 会对比差异然后将更新应用到重用的 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)
}
相关推荐
风骏时光牛马3 分钟前
从零到落地的AI系统学习进阶路线
前端
橙光成长2 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
小poop9 小时前
CSS 基础与实战:从入门到精通网页美化
前端
Csvn10 小时前
运行时性能
前端
Never_every9911 小时前
位图转矢量工具哪个好?实测抠抠图AI位图转矢量工具
大数据·前端
paopaokaka_luck11 小时前
宠物门店经营管理系统(销售与库存联动、进货自动记账、 退换与退款关联、员工个人业绩、ECharts 经营分析、滞销识别与客户分层、宠物档案年龄计算)
前端·javascript·spring boot·mysql·echarts·mybatis·1024程序员节
空中湖12 小时前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
欣欣之王来了14 小时前
原始HTML插值:v-html指令的使用与安全风险
前端·学习·架构·项目·vue3教程
IT_陈寒14 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端
计算机魔术师14 小时前
砍半价格、参数再涨,国产旗舰追到 Opus 鼻尖了
前端