《Vue.js 技术与实现》第 11 章 快速 diff 阅读总结

第 11 章主要讲解了 Vue3 在 更新子节点的实现原理。实现原理上使用了快速 diff,它源起于 ivi 和 inferno 这两个框架,Vue 借鉴并扩展它。

本章主要解决如下问题:

  • 处理相同的前置元素
  • 处理相同的后置元素
  • 判断是否需要进行DOM移动操作
  • 如何移动 DOM 元素

处理相同的前置元素

function patchKeyedChildren(n1, n2, container) {
  const newChildren = n2.children;
  const oldChildren = n1.children;

  // 更新相同的前缀节点
  // 索引 j 指向新旧两组子节点的开头
  let j = 0;
  let oldVNode = oldChildren[j];
  let newVNode = newChildren[j];

  // while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
  while (oldVNode.key === newVNode.key) {
    // 调用 patch 函数更新
    patch(oldVNode, newVNode, container);

    j++;

    oldVNode = oldChildren[j];
    newVNode = newChildren[j];
  }
}

处理相同的前置元素时,使用 while 循环向后遍历,让索引 i 递增,直至遇到不同的节点为止。

这里需要注意的是,需要相同 key 值得节点,要调用 patch 函数进行更新。

处理相同的后置元素

// while 循环向前遍历,直到遇到拥有不同 key 值的节点为止
while (oldVNode.key === newVNode.key) {
  // 调用 patch 函数更新
  patch(oldVNode, newVNode, container);
  oldEnd--;
  newEnd--;
  oldVNode = oldChildren[oldEnd];
  newVNode = newChildren[newEnd];
}

在处理完相同前置元素后,再处理相同的后置元素,使用 while 循环向前遍历,不断移动新旧节点尾指针 newEndoldEnd 直到遇到拥有不同 key 值的节点为止。

这里需要注意的是,需要相同 key 值得节点,要调用 patch 函数进行更新。

剩下的新子节点里面新增的节点或者删除的节点,我们接下来继续处理

处理新增和删除的节点

// 满足条件,则说明从 j -> newEnd 之间的节点应作为新节点插入
if (j > oldEnd && j <= newEnd) {
  // 锚点的索引
  const anchorIndex = newEnd + 1;
  // 锚点元素
  const anchor =
    anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;
  // 采用 while 循环,调用 patch 函数逐个挂载新增的节点
  while (j <= newEnd) {
    patch(null, newChildren[j++], container, anchor);
  }
} else if (j > newEnd && j <= oldEnd) {
  // j -> oldEnd 之间的节点应该被卸载
  while (j <= oldEnd) {
    unmount(oldChildren[j++]);
  }
}

j > oldEnd && j <= newEnd ,代表旧的子节点已经遍历完,新的子节点还有,说明存在新增,需要使用 while 循环挂载 jnewEnd 之间所有的子节点。新增的子节点以下一个子节点作为锚点元素,挂载在下一个锚点元素前面,如果没有下一个锚点元素,说明是当前节点就是最后一个。

j > newEnd && j <= oldEnd,代表新的子节点已经遍历完,旧的子节点还有,说明存在删除,需要使用 while 循环卸载 j 到 oldEnd 之间所有的子节点。

判断是否需要进行DOM移动操作和如何移动?

经过处理完相同前置元素 和 处理完相同后置元素后还有中间部分的节点无法处理,我们中间肯还存在元素无法进行处理,比如如下新旧子节点结构,在处理完前置和后置元素后中间还有一些差异的子节点需要 patch 更新的。

新子节点           旧子节点
p - 1							p - 1
----------------------------
p - 3							p - 2
p - 4							p - 3
p - 2							p - 4
p - 7							p - 6
----------------------------
p - 5							p - 5

接下来我们需要解决的问题就是这些节点

  • 判断是否需要移动
  • 如何移动
  • 找出那些需要新增或删除的节点

定义 source 数组保存新节点在旧节点中的下标

source 数组用于存储新子节点在旧子节点中的位置索引,后续将使用它计算出一个最长递增子序列,并用于辅助完成 DOM 的移动操作。

这个 source 变量在源码中命名是 newIndexToOldIndexMap

  1. 新增一个 source 数组,长度为新子节点预处理之后的节点数量,初始值填充为 -1,用来存储新子节点在旧子节点中的位置索引
  2. 为新节点建立一个索引表,保存节点 key 与位置索引之间的映射
  3. 遍历旧节点,旧节点的 key 在索引表中查找旧节点在新节点的索引,保存在 k 中
  4. 如果存在 k,说明旧节点在新节点中找到了,调用 patch 更新,填充 source 数组,否则直接卸载。
// 构造 source 数组
const count = newEnd - j + 1; // 新的一组子节点中剩余未处理节点的数量
const source = new Array(count);
source.fill(-1);

const oldStart = j;
const newStart = j;
// 索引表
const keyIndex = {};

for (let i = newStart; i <= newEnd; i++) {
  keyIndex[newChildren[i].key] = i;
}

for (let i = oldStart; i <= oldEnd; i++) {
  oldVNode = oldChildren[i];
  // k 是旧节点在新节点中的索引
  const k = keyIndex[oldVNode.key];

  if (typeof k !== 'undefined') {
    newVNode = newChildren[k];
    patch(oldVNode, newVNode, container);
    source[k - newStart] = i;
  } else {
    // 没找到
    unmount(oldVNode);
  }
}

判断是否需要进行DOM移动操作

const count = newEnd - j + 1; // 新的一组子节点中剩余未处理节点的数量
let moved = false;
let pos = 0;
let patched = 0;

for (let i = oldStart; i <= oldEnd; i++) {
  oldVNode = oldChildren[i];
  if (patched < count) {
    const k = keyIndex[oldVNode.key];
    if (typeof k !== 'undefined') {
      newVNode = newChildren[k];
      patch(oldVNode, newVNode, container);
      patched++;
      source[k - newStart] = i;
      // 判断是否需要移动
      if (k < pos) {
        moved = true;
      } else {
        pos = k;
      }
    } else {
      // 没找到
      unmount(oldVNode);
    }
  } else {
    unmount(oldVNode);
  }
}
  • 定义变量 moved 标识是否需要移动,pos 表示遍历旧子节点过程中遇到的最大索引值
  • 如果遍历过程遇到的索引值呈递增趋势,说明不需要节点移动,否则需要。有了 moved 标识,就知道了是否需要移动节点
  • 定义变量 patched 表示已更新节点数量,如果更新节点数量大于新的一组节点需要更新的数量,说明有多余的节点,需要卸载

如何移动?

if (moved) {
  const seq = getRequence(source);
  // s 指向最长递增子序列的最后一个值
  let s = seq.length - 1;
  let i = count - 1;
  for (i; i >= 0; i--) {
    if (source[i] === -1) {
      // 说明索引为 i 的节点是全新的节点,应该将其挂载
      // 该节点在新 children 中的真实位置索引
      const pos = i + newStart;
      const newVNode = newChildren[pos];
      // 该节点下一个节点的位置索引
      const nextPos = pos + 1;
      // 锚点
      const anchor =
        nextPos < newChildren.length ? newChildren[nextPos].el : null;
      // 挂载
      patch(null, newVNode, container, anchor);
    } else if (i !== seq[s]) {
      // 说明该节点需要移动
      // 该节点在新的一组子节点中的真实位置索引
      const pos = i + newStart;
      const newVNode = newChildren[pos];
      // 该节点下一个节点的位置索引
      const nextPos = pos + 1;
      // 锚点
      const anchor =
        nextPos < newChildren.length ? newChildren[nextPos].el : null;
      // 移动
      insert(newVNode.el, container, anchor);
    } else {
      // 当 i === seq[j] 时,说明该位置的节点不需要移动
      // 并让 s 指向下一个位置
      s--;
    }
  }
}

通过 getRequence 获取 source 数组的最长递增子序列,找到不需要移动的节点

关于最长递增子序列看下我这篇文章:Vue3 最长递增子序列详解

seq  source    KeyIndex     新节点       旧节点
                            
0  ↑   2          3:1       p - 3       p - 2
1 (s)  3          4:2       p - 4       p - 3
       1          2:3       p - 2 ↑     p - 4
      -1          7:4       p - 7(i)    p - 6

倒序遍历节点,如果 source[i] 为 默认值 -1,说明该节点时新增的节点,需要挂载。

如果遍历的节点下标不是最长递增子序列 seq[s] 说明是需要移动的节点,设置锚点为上一个已经更新的节点为锚点位置,将当前节点移动到锚点前面。

如果遍历的节点下标是最长递增子序列 seq[s] 说明是不需要移动节点,s— 即可

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值