Vue3 Diff 算法简易版

背景

学习一下Vue3中的diff算法~

逻辑概述

这个算法的过程不算太复杂:

  • 同时从新旧列表的头部遍历,找出相同的节点,则patch,否则就停止遍历;
  • 同时从新旧列表的尾部遍历,找出相同的节点,则patch,否则就停止遍历;
  • 如果旧列表的节点都遍历过了,新列表还有节点没有被遍历,那么说明新列表增加了节点,则将这些节点全部新增;
  • 如果此时旧列表还有节点没有被遍历过、新列表已经都遍历完,那么说明新列表删除了旧列表的某些节点,则将旧列表中未被遍历过的节点删除;
  • 如果新旧列表中都存在未遍历过的节点,则需要操作这些节点,这里的操作指的是新增、移动、删除,这一步骤是最复杂的,涉及到了二分查找和最长升上子序列。

Diff过程

函数传参

/*** Vue3 Diff简易版* @param {*} oldList 旧列表* @param {*} newList 新列表* @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作*/
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {// 节点是否可以复用,这里只是简单的判断key值,事实上判断元素是否可以复用,还要判断tag等值function isSameVnodeType(node1, node2) {return node1.key === node2.key;}// ...
}

从头遍历新旧列表,找出相同的节点

在这里插入图片描述
在这张图中,我们同时从两个列表的头部遍历,相同的节点有A B,然后停止了循环:

function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {// 节点是否可以复用function isSameVnodeType(node1, node2) {return node1.key === node2.key;}// 头部遍历的起始变量let i = 0;// 旧列表的长度const oldLen = oldList.lenght;// 新列表的长度const newLen = newList.lenght;// 1. 从头遍历新旧列表,找出相同的节点while(i < oldLen && i < newLen) {const oldNode = oldList[i];const newNode = newList[i];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);i++;}else {break;}}
}

从尾遍历新旧列表,找出相同的节点

在这里插入图片描述
由于新旧列表的长度不一定相同,所以两者遍历的尾巴坐标也不一样,所以需要单独声明,找到相同的节点就继续往前走,否则就退出循环:

// 旧列表尾巴下标
const oldEndIndex = oldLen - 1;
// 新列表尾巴下标
const newEndIndex = newLen - 1; // 2.从尾遍历新旧列表,找出相同的节点
while(i <= oldEndIndex && i <= newEndIndex){const oldNode = oldList[oldEndIndex];const newNode = newList[newEndIndex];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);oldEndIndex--;newEndIndex--}else {break;}
}

新增节点

在这里插入图片描述
之前的i在最后循环的时候,最后的值为2,而oldEndIndex = 1, newEndIndex = 2,则说明新列表比旧列表多出了一个节点,即新增节点。

// 3.新增节点
if( i > oldEndIndex && i <= newEndIndex ){while(i <= newEndIndex) {const newNode = newList[i];mountElement(newNode.key);i++;}}

删除节点

在这里插入图片描述
如前面所说,此时旧列表还有节点没有被遍历过、新列表已经遍历完,那么说明新列表删除了旧列表的某些节点,则将旧列表中未被遍历过的节点删除,由图可知,C节点在新列表中被删除了。

// 4. 删除节点
else if(i <= oldEndIndex && i > newEndIndex) {while(i <= oldEndIndex) {const delNode = oldList[i];unmount(delNode.key);i++;}
}

到这里的代码整体如下:

/*** Vue3 Diff简易版* @param {*} oldList 旧列表* @param {*} newList 新列表* @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作*/
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {// 节点是否可以复用function isSameVnodeType(node1, node2) {return node1.key === node2.key;}let i = 0;// 旧列表的长度const oldLen = oldList.lenght;// 新列表的长度const newLen = newList.lenght;// 旧列表尾巴下标const oldEndIndex = oldLen - 1;// 新列表尾巴下标const newEndIndex = newLen - 1; // 1. 从头遍历新旧列表,找出相同的节点while(i < oldLen && i < newLen) {const oldNode = oldList[i];const newNode = newList[i];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);i++;}else {break;}}// 2.从尾遍历新旧列表,找出相同的节点while(i <= oldEndIndex && i <= newEndIndex){const oldNode = oldList[oldEndIndex];const newNode = newList[newEndIndex];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);oldEndIndex--;newEndIndex--}else {break;}}// 3.新增节点if( i > oldEndIndex && i <= newEndIndex ){while(i <= newEndIndex) {const newNode = newList[i];mountElement(newNode.key);i++;}}// 4.删除节点else if(i <= oldEndIndex && i > newEndIndex) {while(i <= oldEndIndex) {const delNode = oldList[i];unmount(delNode.key);i++;}}else {// ...} 
}

最后的处理

好了接下来进入最烧脑的环节,先给个例子先:
在这里插入图片描述
到这里我们其实就是要将新列表中的[M, H,.C, D, E, J]和旧列表中的[C, E, H]进行对比从而做出操作。源码肯定不会那么傻先遍历新列表的节点,再内嵌循环旧列表节点一个个做对比,他们根据数组的特点,转换了数据结构,减少了循环的耗时。接下来跟着源码学习:

// 新旧节点不同的起点坐标
const oldStartIndex = i;
const newStartIndex = i;// 5.1 根据数组的特点,将新列表中的key和index做成映射关系
const keyToNewIndexMap = new Map();
for(let i = newStartIndex; i < newEndIndex; i++){const node = newList[i];keyToNewIndexMap.set(node.key, i);
}

得出来的结果如下:

在这里插入图片描述
这里先提一下相对坐标!!!
Vue源码中为了方便,直接将循环的范围变成了我们要操作的新列表的这些节点中(即下面会用到相对坐标),比如M的相对要操作的节点列表的坐标是0,而不是原来列表中的2。
继续,接下来声明一些我们后面会用到的变量:

// 新列表中需要操作的节点数量(上述例子就是6个节点)
const toBePatched = newEndIndex - newStartIndex + 1;
// 已经操作过的节点数量
let patched = 0;
// 先将这个参数简单理解为,新列表的节点与旧列表节点的映射,如果存在就非0,如果不存在就是0
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

然后我们现在先遍历旧列表:

for(let i = oldStartIndex; i < oldEndIndex; i++){const node = oldList[i];// 如果需要操作的节点数量已经小于操作过的节点,说明旧列表中这些节点是木有用的,需要卸载if(patched >= toBePatched){unmount(node.key);continue;}// 接下来根据oldNode中的key,来看看keyToNewIndexMap是否有对应的下标,如果有,那说明旧节点在新列表中被复用啦,否则就得删除// Vue3源码中有一段是处理节点没有key的情况,这里就不写啦,这里默认我们的节点都是有key的const newIndex = keyToNewIndexMap.get(node.key);if(newIndex === undefined){// 旧节点在新列表中没有被复用,直接卸载unmount(node.key);}else {// 如果在旧列表中找到可以复用的节点,那么更新newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1// 这里加1的原因 后面再解释!newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;// 复用节点patch(node.key);// 操作过的节点数量 + 1patched++;}
}

那么这里我们先得出newIndexToOldIndexMap的值:
在这里插入图片描述
newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1这里加一的原因我理解的是,这个数组如果第一个元素在新旧列表都存在,那么它的坐标就是0,所以为了区分是否存在,需要+1,确保称为非0数。早期看技术文,也有初始化为-1的情况,这种就不用+1了。
那么这里就获取到了一个数组坐标,接下来的精华就是根据这个数组坐标,来对我们的节点进行操作,操作之前需要先获得最长升上子序列,好了这个算法我们先跳过实现过程(算法解说不适合我),先说一下为什么是最长升上子序列。我们在操作节点的时候,是不是希望有比较多的节点保持位置不变,尽量改变较少的节点。
我们得出的数组是[0, 5, 3, 0, 4, 0],忽略0(不是复用的节点)这里我们不难看出来连续的最长子序列是[3, 4]对应的是节点C、E,我们只要移动节点D即可。
不过这里最长子序列的结果应该返回的是[3, 4]的坐标,所以结果应该是[2, 4]
在这里插入图片描述

// 先假设获得最长升上子序列
function getSequence(arr){// 这里应该返回的是序列对应的下标,[3, 4] 转为下标变成[2, 4]return [2, 4]
}

然后继续我们的逻辑:

for(let i = oldStartIndex; i < oldEndIndex; i++){// ...
}// 最长升上子序列
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
// 最长升上子序列的最后一个坐标
let lastIndex = increasingNewIndexSequence.length - 1;
/**
* 由于移动元素可能会用到inertBefore方法
* 该方法需要知道后一个元素,才能插入前面一个元素
* 所以这次遍历需要从后面开始
*/
for (i = toBePatched - 1; i >= 0; i--){if(newIndexToOldIndexMap[i] === 0) {// 说明是新增节点mountElement(node.key);}else {// 将相对坐标转为绝对(列表)坐标const index = newStartIndex + i;// 获得对应新列表中的节点const node = newList[index];// 如果没有最长上升子序列 或者 当前节点不在该子序列中,则需要移动if(lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {move(node.key)}else {lastIndex--;}}}

在这里插入图片描述

newIndexToOldIndexMap从后往前遍历:

  • 0代表新增节点,向前移动
  • 当前i为4, increasingNewSequence[lastIndex] = 4,相等,向前移动,lastIndex–
  • 0代表新增节点,向前移动
  • 当前i为2, increasingNewSequence[lastIndex] = 2,相等,向前移动,lastIndex–
  • 由于lastIndex小于0,所以移动节点5
  • 0代表新增节点,向前移动,结束循环

这里有个优化的点,就是获得最长升上子序列这个函数其实挺耗时的,我们在某些情况下,其实并不一定要使用它:

// 是否需要获得最长升上子序列(是否需要移动节点)
let move = false;
// 子序列中最大的值
let maxNewIndexSoFar = 0;for(let i = oldStartIndex; i < oldEndIndex; i++){// ...if(newIndex === undefined) {// ...}else {// 判断是否有人插队(每个节点都按需递增的话)if(newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex;}else {// 说明被插队了,得去求最长递增子序列move = true;}// ...}
}
// 获取最长子序列(可能很耗时,所以要进行优化,判断是否确定要move)
const increasingNewIndexSequence = move ? getSequence(newIndexToOldIndexMap) : [];

所以最后的代码就是:

/*** Vue3 Diff简易版* @param {*} oldList 旧列表* @param {*} newList 新列表* @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作*/
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {// 节点是否可以复用function isSameVnodeType(node1, node2) {return node1.key === node2.key;}let i = 0;// 旧列表的长度const oldLen = oldList.lenght;// 新列表的长度const newLen = newList.lenght;// 旧列表尾巴下标const oldEndIndex = oldLen - 1;// 新列表尾巴下标const newEndIndex = newLen - 1; // 1. 从头遍历新旧列表,找出相同的节点while(i < oldLen && i < newLen) {const oldNode = oldList[i];const newNode = newList[i];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);i++;}else {break;}}// 2.从尾遍历新旧列表,找出相同的节点while(i <= oldEndIndex && i <= newEndIndex){const oldNode = oldList[oldEndIndex];const newNode = newList[newEndIndex];if(isSameVnodeType(oldNode, newNode)) {patch(oldNode.key);oldEndIndex--;newEndIndex--}else {break;}}// 3.新增节点if( i > oldEndIndex && i <= newEndIndex ){while(i <= newEndIndex) {const newNode = newList[i];mountElement(newNode.key);i++;}}// 4.删除节点else if(i <= oldEndIndex && i > newEndIndex) {while(i <= oldEndIndex) {const delNode = oldList[i];unmount(delNode.key);i++;}}else {// 新旧节点不同的起点坐标const oldStartIndex = i;const newStartIndex = i;// 5.1 根据数组的特点,将新列表中的key和index做成映射关系const keyToNewIndexMap = new Map();for(let i = newStartIndex; i < newEndIndex; i++) {const node = newList[i];keyToNewIndexMap.set(node.key, i);}// 新列表中需要操作的节点数量(上述例子就是6个节点)const toBePatched = newEndIndex - newStartIndex + 1;// 已经操作过的节点数量let patched = 0;// 先将这个参数简单理解为,新列表的节点与旧列表节点的映射,如果存在就非0,如果不存在就是0const newIndexToOldIndexMap = new Array(toBePatched).fill(0);// 是否需要获得最长升上子序列(是否需要移动节点)let moved = false;// 子序列中最大的值let maxNewIndexSoFar = 0;for(let i = oldStartIndex; i < oldEndIndex; i++) {const node = oldList[i];// 如果需要操作的节点数量已经小于操作过的节点,说明旧列表中这些节点是木有用的,需要卸载if(patched >= toBePatched) {unmount(node.key);continue;}// 接下来根据oldNode中的key,来看看keyToNewIndexMap是否有对应的下标,如果有,那说明旧节点在新列表中被复用啦,否则就得删除// Vue3源码中有一段是处理节点没有key的情况,这里就不写啦,这里默认我们的节点都是有key的const newIndex = keyToNewIndexMap.get(node.key);if(newIndex === undefined) {// 旧节点在新列表中没有被复用,直接卸载unmount(node.key);}else {// 判断是否有人插队(每个节点都按需递增的话)if(newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex;}else {// 说明被插队了,得去求最长递增子序列moved = true;}// 如果在旧列表中找到可以复用的节点,那么更新newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;// 复用节点patch(node.key);// 操作过的节点数量 + 1patched++;}}// 获取最长子序列(可能很耗时,所以要进行优化,判断是否确定要move)const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [];let lastIndex = increasingNewIndexSequence.length - 1;/*** 由于移动元素可能会用到inertBefore方法* 该方法需要知道后一个元素,才能插入前面一个元素* 所以这次遍历需要从后面开始*/for(let i = toBePatched - 1; i > 0; i--) {if(newIndexToOldIndexMap[i] === 0) {// 判断节点是不是新增的,不能被复用,即新增节点mountElement(node.key);}else {// 将相对坐标转为绝对(列表)坐标const index = newStartIndex + i;// 获得对应新列表中的节点const node = newList[index];// 如果没有最长上升子序列 或者 当前节点不在该子序列中,则需要移动if(lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {move(node.key)}else {lastIndex--;}}}}
}

参考链接

  • 解锁vue3 diff算法
  • React、Vue2、Vue3的三种Diff算法
  • 300. 最长递增子序列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/15809.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASM Java字节码操作框架入门学习 输出Hello World

ASM Java字节码操作框架入门学习 输出Hello World 1.类信息 package org.example;public class Hello {public void say(){System.out.println("hello world");}}查看字节码信息 //动态设置栈大小ClassWriter classWriter new ClassWriter(ClassWriter.COMPUTE_FR…

在vscode中配置git bash终端

将以下配置添加到vscode中的settings.json中 "terminal.integrated.profiles.windows": {"PowerShell": {"source": "PowerShell","icon": "terminal-powershell"},"Command Prompt": {"path"…

Vue3+Vite 项目配置 vue-router,并完成路由模块化

前言 我的技术栈&#xff1a;Vue3 Vite TypeScirpt我的包管理工具&#xff1a;pnpm&#xff08;v8.6.6&#xff09;我的 node.js 版本&#xff1a;v16.14.0 一、安装vue-router pnpm install vue-router二、创建页面 在 /src/views 文件夹下创建 home、login、test文件夹…

【数据挖掘从入门到实战】——专栏导读

目录 1、专栏大纲 &#x1f40b;基础部分 &#x1f40b;实战部分 &#x1f40b;竞赛部分 2、代码附录 数据挖掘专栏&#xff0c;包含基本的数据挖掘算法分析和实战&#xff0c;数据挖掘竞赛干货分享等。数据挖掘是从大规模数据集中发现隐藏模式、关联和知识的过程。它结合…

绝了!阿里大佬的“Redis深度核心笔记“,从基础到源码,全是精华

Redis怎么学习&#xff1f; 我晕了&#xff0c;竟然没人好好回答怎么学习Redis&#xff0c;全都是介绍redis的长文。。。这还让人怎么学。我来分享下我自学Reids看过的资料吧 为什么要学习Redis&#xff1f; Redis 是互联网技术架构在存储系统中使用得最为广泛的中间件&…

【环境配置】Conda报错 requests.exceptions.HTTPError

问题&#xff1a; conda 创建新的虚拟环境时报错 Collecting package metadata (current_repodata.json): done Solving environment: done# >>>>>>>>>>>>>>>>>>>>>> ERROR REPORT <<<<<<…

Installation request for phpoffice/phpspreadsheet

办法 composer update --ignore-platform-reqs

SwiftUI的优缺点

2019年WWDC大会上&#xff0c;苹果在压轴环节向大众宣布了基于Swift语言构建的全新UI框架——SwiftUI&#xff0c;开发者可通过它快速为所有的Apple平台创建美观、动态的应用程序。推荐大量使用struct代替类。 SwiftUI 就是⼀种声明式的构建界面的用户接口工具包。 SwiftUI使用…

会话机制【Cookie 和 Session】,登陆页面的模拟实现

前言 小亭子正在努力的学习编程&#xff0c;接下来将开启JavaEE的学习~~ 分享的文章都是学习的笔记和感悟&#xff0c;如有不妥之处希望大佬们批评指正~~ 同时如果本文对你有帮助的话&#xff0c;烦请点赞关注支持一波, 感激不尽~~ 目录 前言 Cookie 和 Session 是什么 Cookie…

Android Binder进程间通讯原理分析

Binder IPC原理 Android系统是基于Linux内核开发的。Linux开发提供了丰富的进程间通讯机制&#xff0c;例如管道、信号、消息队列、共享内存、插口&#xff08;Socket&#xff09; 。而Binder是一套新的通讯工具。 Binder通信采用了c/s架构&#xff0c;所以我们包含了 Client&…

LwIP系列(5):TCP 3次握手+4次挥手+状态机转换

前言 TCP的3次握手、4次挥手以及TCP状态机&#xff0c;是TCP的核心概念&#xff0c;我们在分析LwIp中TCP相关代码流程&#xff0c;也需要熟悉这些流程&#xff0c;本文就详细介绍这些概念。 TCP 3次握手、应用数据交互、4次挥手完整流程 TCP 为什么是3次握手&#xff0c;而不…

【设计模式】第十九章:访问者模式详解及应用案例

系列文章 【设计模式】七大设计原则 【设计模式】第一章&#xff1a;单例模式 【设计模式】第二章&#xff1a;工厂模式 【设计模式】第三章&#xff1a;建造者模式 【设计模式】第四章&#xff1a;原型模式 【设计模式】第五章&#xff1a;适配器模式 【设计模式】第六章&…