当前位置: 首页 > news >正文

VUE DIFF算法之快速DIFF

VUE DIFF算法系列讲解

VUE 简单DIFF算法
VUE 双端DIFF算法


文章目录

  • VUE DIFF算法系列讲解
  • 前言
  • 一、快速DIFF的代码实现
  • 二、实践
    • 练习1
    • 练习2
  • 总结


前言

本节我们来写一下VUE3中新的DIFF算法-快速DIFF,顾名思义,也就是目前最快的DIFF算法(在VUE中)
本节内容值包括快速DIFF算法的实现,不考虑最长递增子序列算法的实现


一、快速DIFF的代码实现

相对于我们此系列的前两种DIFF算法,此DIFF算法在进入真正的DIFF算法之前,会有一个预处理的过程,这个主要是借鉴了纯文本Diff算法的思想。下边,我们就直接通过代码看下整个Diff的实现流程。

const oldChildren = n1.children;
const newChildren = n2.children;// 预处理开始
// 处理新旧子节点的头部可复用节点
let j = 0;
let oldVNode = oldChildren[j];
let newVNode = newChildren[j];
while (oldVNode.key === newVNode.key) {// 打补丁patch(oldVNode, newVNode, container);// 更新VNodej++;oldVNode = oldChildren[j];newVNode = newChildren[j];
}// 处理新旧节点尾部可复用节点, 因为新旧子节点长度可能不一致,所以需要定义两个变量
let oldEnd = oldChildren.length - 1;
let newEnd = newChildren.length - 1;oldVNode = oldChildren[oldEnd];
newVNode = newChildren[newEnd];while (oldVNode.key === newVNode.key) {// 打补丁patch(oldVNode, newVNode, container);// 更新VNodeoldEnd--;newEnd--;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];
}
// 预处理结束后// 预处理结束会有三种情况:1.新子节点有剩余;2.旧街店有剩余;3:新旧节点均有剩余
if (j > oldEnd && j <= newEnd) {// 如果新子节点有剩余// 获取锚点idlet anchorIdx = newEnd + 1;// 获取锚点node,如果let anchor = anchorIdx === newChildren.length ? newChildren[anchorIdx].el : null;while (j <= newEnd) {// 挂载patch(null, newChildren[j++], container, anchor);}
} else if (j > newEnd && j <= oldEnd) {// 如果旧节点有剩余,则需要卸载while (j <= oldEnd) {unmount(oldChildren[j++]);}
} else {// 如果新旧节点均有剩余// 构造source数组const count = newEnd - j + 1;let source = new Array(count);source.fill(-1);// 新旧子节点的起始indexconst oldStart = j;const newStart = j;let pos = 0;let moved = false;// 构建新子节点的{key: index} 对象const keyIndex = {};for (let i = newStart; i < newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 保存已被处理过的旧子节点数let patched = 0;// 填充sourcefor (let i = oldStart; i <= oldEnd; i++) {oldVNode = oldChildren[i];if (patched <= count) {const k = keyIndex[oldVNode.key];if (k !== undefined) {// 打补丁newVNode = newChildren[i];patch(oldVNode, newVNode, container);patched++;source[k - newStart] = i;if (k < pos) {moved = true;} else {pos = k;}} else {// 没有找到,卸载unmount(oldChildren[i]);}} else {// 处理数已等于新子节点数,其余卸载unmount(oldChildren[i]);}}// 如果需要移动if (moved) {const seq = lis(source);let s = seq.length - 1;let i = count - 1;for (i; i >= 0; i--)if (source[i] === -1) {// 这种情况属于新增const pos = i + newStart;const newVNode = newChildren[pos];// 获取锚点的索引const newPos = pos + 1;// 锚点const anchor = newPos < newChildren.length ? newChildren[newPos] : null;// 挂载patch(null, newVNode, container, anchor);} else if (seq[s] !== i) {// 这种情况需要移动const pos = i + newStart;const newVNode = newChildren[pos];// 获取锚点的索引const newPos = pos + 1;// 锚点const anchor = newPos < newChildren.length ? newChildren[newPos] : null;// 挂载insert(newVNode, container, anchor);} else {// 当 i === seq[j] 时,说明该位置的节点不需要移动// 并让 s 指向下一个位置s--;}}
}

二、实践

练习1

// 旧子节点
p-1 p-2 p-3
// 新子节点
p-1 p-3// 预处理开始
// while循环对比,预处理头部可服用量节点
let j = 0
newVNode = p-1
oldVNode = p-1
// 第一次循环 j=0
newVNode.key === oldVNode.key 可复用 j++
newVNode = p-2
oldVNode = p-3
// 第二次循环
newVNode.key !== oldVNode.key 跳出循环//  while循环对比,预处理尾部可服用量节点
let oldEnd = 2(oldChildren.length - 1);
let newEnd = 1(newChildren.length - 1);
oldVNode = p-3(oldChildren[oldEnd]);
newVNode = p-3(newChildren[newEnd]);
// 第一次循环 oldEnd=2 newEnd=1
newVNode.key === oldVNode.key 可复用 oldEnd-- newEnd--
newVNode = p-2
oldVNode = p-1
// 第二次循环 oldEnd=1 newEnd=0
newVNode.key !== oldVNode.key 跳出循环
// 预处理结束// 预处理结束,此时j(1)>newEnd(0) && j(1)<=oldEnd(1),说明旧的子节点有剩余,仅需要将剩余子节点卸载即可

练习2

// 旧子节点
p-1 p-2 p-3 p-4 p-6 p-5
// 新子节点
p-1 p-3 p-4 p-2 p-7 p-5// 预处理开始
// while循环对比,预处理头部可服用量节点
let j = 0
newVNode = p-1
oldVNode = p-1
// 第一次循环 j=0
newVNode.key === oldVNode.key 可复用 j++
newVNode = p-2
oldVNode = p-3
// 第二次循环
newVNode.key !== oldVNode.key 跳出循环//  while循环对比,预处理尾部可服用量节点
let oldEnd = 5(oldChildren.length - 1);
let newEnd = 5(newChildren.length - 1);
oldVNode = p-5(oldChildren[oldEnd]);
newVNode = p-5(newChildren[newEnd]);
// 第一次循环 oldEnd=2 newEnd=1
newVNode.key(p-5) === oldVNode.key(p-5) 可复用 oldEnd-- newEnd--
newVNode = p-7
oldVNode = p-6
// 第二次循环 oldEnd=4 newEnd=4
newVNode.key !== oldVNode.key 跳出循环
// 预处理结束// 此时真实dom节点如下
p-1() p-2 p-3 p-4 p-6 p-5()
// 待处理
p-2 p-3 p-4 p-6---------------------------------------------
// 预处理结束,此时j(1)<=newEnd(4) && j(1)<=oldEnd(4),说明新旧的子节点有剩余,此时我们需要构建一个sourece数组,这个数组长度为新子节点在预处理后剩余未处理的节点数,并且初始值为-1// 新子节点未处理完的节点数
const count = newEnd - j + 1;
// 构建source数组
let source = new Array(count);
source.fill(-1);
// 定义新旧子节点的起始index
const oldStart = j(1);
const newStart = j(1);
// 定义在旧子节点便利过程中遇到的最大的索引值
let pos = 0;
// 代表是否需要移动节点
let moved = false;
// 定义一个数量表示,表示已经处理过的子节点数.如果已处理过的子节点数超过新子节点数,则表示其余的为多余节点,直接卸载
let patched = 0// 构建一个索引表,此表内存储新子节点中,key与index的映射
keyIndex = {p-3 : 1,p-4 : 2,p-2 : 3,p-7 : 4
}// 填充source数组并处理多余oldVNode
// 开始循环 
// 第一次循环 i(oldStart) = 1; oldEnd = 4; count = 4; pos = 0;
oldVNode    patched      kp-2          0         3
k !== undefined patched++ pos=k i++
source = [-1, -1, 1, -1]
// 第二次循环 i = 2; oldEnd = 4; count = 4; pos = 3
oldVNode    patched      kp-3          1         1
k !== undefined patched++ moved = true
source = [2, -1, 1, -1]
// 第三次循环 i = 3; oldEnd = 4; count = 4; 因moved为true,所以不再需要关心pos
oldVNode    patched      kp-4          2         2
k !== undefined patched++ 
source = [2, 3, 1, -1]
// 第四次循环 i = 4; oldEnd = 4; count = 4; 
oldVNode    patched      kp-6          3      undefined
k === undefined 卸载
source = [2, 3, 1, -1]
// 第四次循环 i = 5; oldEnd = 4; i > oldEnd 跳出循环// 此时真实dom节点如下
p-1() p-2 p-3 p-4 p-6() p-5()
// 待处理
p-2 p-3 p-4
---------------------------------------------------------// 因moved为true,所以需要进行移动
// 构建一个最长递增子序列(此节中暂不考虑最长递增子序列算法)
const seq = [0, 1]
// 定义s, 其值为最长递增子序列的最大index
let s = 1
// 定义i, 其值为source的最大index
let i = 3// 开始循环
// 第一次循环 i = 3;
source[3] = -1, 则此节点为新增节点,需要挂载,锚点为p-5
// 此时真实dom节点如下
p-1() p-2 p-3 p-4 p-7() p-5()// 第二次循环 i = 2;
source[2] = 1, 
seq[1] !== 2 此节点需要移动, 锚点为p-7
// 此时真实dom节点如下
p-1() p-3 p-4 p-2() p-7() p-5()// 第三次循环 i = 1;
source[1] = 3, 
seq[1] === 1 此节点不需要移动 s--
// 此时真实dom节点如下
p-1() p-3 p-4() p-2() p-7() p-5()// 第四次循环 i = 0;
source[0] = 2, 
seq[0] === 0 此节点不需要移动
// 此时真实dom节点如下
p-1() p-3() p-4() p-2() p-7() p-5()

总结

快速Diff算法的整体逻辑还是比较容易理解的,其中比较巧妙的是预处理和source的用法。学习快速Diff算法不仅让我们更深刻的了解了VUE3,同时其中的一些核心逻辑,也可以为我们的日常开发工作中提供一些开发思路。

参考:<<vue设计与实现>>第10章
github:link

http://www.lryc.cn/news/21684.html

相关文章:

  • 一文掌握如何轻松稿定项目风险管理【静说】
  • 操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权
  • ecology9-谷歌浏览器下-pdf.js在渲染时部分发票丢失文字 问题定位及解决
  • JavaScript Window Navigator
  • Linux基础命令-du查看文件的大小
  • 文献计量分析方法:Citespace安装教程
  • MVI 架构更佳实践:支持 LiveData 属性监听
  • LeetCode438 找到字符串中所有字母异位词 带输入和输出
  • ACSC 2023 比赛复现
  • 【Linux驱动开发100问】什么是模块?如何编写和使用模块?
  • Android 9.0 Recent列表不显示某个app
  • 深度学习之卷积神经网络学习笔记一
  • 黑盒测试的常用方法
  • 操作系统笔记-第一章
  • daillist
  • vue中render函数的作用和参数(vue2中render函数用法)
  • 基于Istio的高级流量管理二(Envoy流量劫持、Istio架构、高级流量管理)
  • Sharding-Springboot-mybatis-plus整合(三)-inline策略
  • 编码的基本概念
  • 函数指针与指针函数的区别
  • 死锁的四个必要条件以及如何避免死锁
  • 浏览器多线程到事件循环机制
  • Lambda表达式的本质
  • 类的加载过程(生命周期)
  • 2023最新谷粒商城笔记之MQ消息队列篇(全文总共13万字,超详细)
  • 多变量线性回归模型
  • php 基于ICMP协议实现一个ping命令
  • Java基本数据类型
  • English Learning - L2 语音作业打卡 Day2 2023.2.22 周三
  • 45. 跳跃游戏 II