虚拟 DOM 和 diff 算法 -03

650 阅读4分钟

本文是关于虚拟 DOM 和 diff 算法的学习笔记,目的在于更好的理解 Vue 的底层原理,篇幅较长,故而拆分为几篇,今后将陆续更新。
之前内容传送门:《虚拟 DOM 和 diff 算法 -01》 《虚拟 DOM 和 diff 算法 -02》

承接前两篇的内容,本篇开始着手当 oldVnode 和 newVnode 是同一节点的情况下的精细化比较。

diff 处理新旧节点为同一节点

分析

精细化比较部分我们继续画流程图(注意:我们手写实现的 diff 算法中,虚拟节点里 children 属性和 text 属性是互斥的,二者有且只有一个有值)

微信截图_20240306100814.png

事实上,主要的问题在于新旧节点的 text 和 children 属性是否有值的 4(2x2) 种情况的处理,而当新节点 text 有值时,旧节点的 text 有值或 children 有值这 2 种情况的处理都是一样的,即直接给旧节点的真实 DOM 的 innerText 赋值,所以其实只有 3 种情况的处理:

  • 新节点的 text 属性有值
  1. 此时旧节点的 text 和 children 属性是什么情况无所谓,直接 innerText 赋值
  • 新节点的 children 属性有值
  1. 旧节点的 text 属性有值
  2. 旧节点的 children 属性有值

新节点 text 有值 或 旧节点的 text 有值

下面开始手写上面分析的 1 和 2 两种情况。此时新建 patchVnode.js 处理:

// patchVnode.js
import creatElement from './creatElement.js'
import updateChildren from './updateChildren.js'

// 处理新旧虚拟节点为同一节点的情况
export default (oldVnode, newVnode) => {
  // oldVnode 和 newVnode 是否为内存中同一对象
  if (oldVnode === newVnode) return
  // newVnode 的 text 属性有没有值
  if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
    // 有 text
    // 新旧虚拟节点的 text 的值是否一样
    if (newVnode.text !== oldVnode.text) {
      // oldVnode.elm 就是旧虚拟节点的真实 DOM
      oldVnode.elm.innerText = newVnode.text
    }
  } else {
    // 没有 text (说明 newVnode.children 有值 )
    // 判断旧节点是 children 有值还是 text 有值
    if (oldVnode.text !== undefined && (oldVnode.children === undefined || oldVnode.children.length === 0)) {
      // 旧节点 text 属性有值,新节点 children 属性有值
      oldVnode.elm.innerHTML = ''
      newVnode.children.forEach(item => {
        const newDom = creatElement(item)
        oldVnode.elm.appendChild(newDom)
      })
    } else {
      // 新旧节点都是 children 属性有值
      updateChildren(oldVnode.elm, oldVnode.children, newVnode.children) // 后面解释
    }
  }
}

在 patch.js 处理新旧为同一节点的地方引入 patchVnode.js:

if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
  // 同一节点
  patchVnode(oldVnode, newVnode)
}

新旧节点均为 children 属性有值

即上面分析的第 3 种情况。我们在对比新旧节点的 children 里的每个子虚拟节点的时候,需要这些节点都定义了 key 属性,不然不定义都是 undefined 就都判断为同一节点了。所以首先来补充一下之前生成虚拟节点时忽略掉的 key 属性:

// vnode.js
export default (sel, data, children, text, elm) => {
  const { key } = data
  return { sel, data, children, text, elm, key }
}

四种命中查找

处理新旧节点均为 children 有值这种情况,用到了经典的 diff 算法优化策略 —— 四种命中查找:

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前
  4. 新前与旧后

这 4 种命中判断是按 1234 的顺序进行的,一旦命中某一条,则不会进行下一条命中,而是开始进行相应的处理。如果都没命中,则用循环来寻找。

要准备 4 个指针,分别指向“旧前”、“旧后”、“新前”、“新后”,在一个 while 循环语句里进行判断, while 的条件就是旧前 <= 旧后 && 新前 <= 新后;即当旧前的指针来到了旧后的后面或是新前的指针来到了新后的后面时跳出 while 循环。

如果旧节点先循环完毕,说明新节点中有要插入的节点;如果新节点先循环完毕,说明旧节点中有要删除的节点。

1. 新前与旧前

yuque_diagram (1).jpg
首先进行旧前和新前指针指向的节点是否为同一节点的判断,发现 sel 相同均为 li,key 相同均为 A,命中判断,不再进行之后的判断,由 patchVnode 函数处理 h('li', { key: 'A'}, 'A') 和 h('li', { key: 'A'}, 'AAA'),然后让旧前指针下移一位改变指针所指的节点,新前指针也下移一位改变指针所指的节点:

yuque_diagram (2).jpg

然后发现旧前和新前指针指向的节点又是同一节点,就再各自下移一位。如此,直到旧前指针移到了旧后指针的下面或是新前指针移到了新后指针的下面,跳出 while 循环语句。

这部分翻译成代码如下(更多细节可查看下一篇文章):

if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode)
  if(newStartVnode) newStartVnode.elm = oldStartVnode?.elm // 给新节点的 elm 赋值,后面有用 
  oldStartVnode = oldCh[++oldStartIdx] 
  newStartVnode = newCh[++newStartIdx]
}

2. 新后与旧后

yuque_diagram (3).jpg
一开始是新前与旧前命中,然后指针各自下移:

yuque_diagram (4).jpg
此时新前和旧前指针指向的节点不是同一节点,则开始判断新后与旧后是否命中,发现命中,则新后与旧后各自上移一位改变指针所指节点,如下图:

yuque_diagram (5).jpg
此时新前 > 新后,跳出 while 循坏。

// 2. 新后与旧后
if (sameVnode(oldEndVnode, newEndVnode)) {
  patchVnode(oldEndVnode, newEndVnode)
  if(newEndVnode) newEndVnode.elm = oldEndVnode?.elm
  oldEndVnode = oldCh[--oldEndIdx]
  newEndVnode = newCh[--newEndIdx]
}

由于接下去还有较多图示,全部挤在一起会使篇幅稍显过长,我将在下篇继续分享剩下的两种命中以及后续内容~

感谢.gif

点赞.png