用通俗语言讲解 vue3中Diff算法完全解析

本文主要是介绍用通俗语言讲解 vue3中Diff算法完全解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

在Vue中 组件初次渲染时,会调用 render 函数生成初始的虚拟 DOM 树。
当组件的状态发生变化时,Vue 会重新调用 render 函数生成新的虚拟 DOM 树。
而Diff 算法是用来比较新旧虚拟 DOM 树的差异,并且只对差异部分进行更新的算法,从而尽量减少性能开销。

虚拟DOM树是什么?

描述组件视图结构的虚拟节点树,也就是VNode树
,它描述了一个 DOM 节点的信息,包括节点类型、属性、子节点等。
实现vNode

function createVNode(type?,props?,children?){const vnode = {type,props,children,}return vnode 
}

运用虚拟 DOM 可以将真实 DOM 的操作转换为JS对象的操作,避免了频繁的直接操作真实 DOM 带来的性能损耗。我们可以运用虚拟DOM的属性来进行操作,vnode 的 children 数组中对应子节点的 vnode 对象,所以在 vue 中通过 vnode 和真实的 DOM 树进行映射,我们也称之为虚拟树。

实现Diff算法

锁定需要改变的位置 处理前置和后置没有改变的元素

预处理前置节点

定义一个头指针

function patchkeyChildren(c1,c2){let i = 0;//c1为旧let e1 = c1.length - 1;let e2 = c2.length - 1;function isSomeVNodeType(n1, n2) {return n1.type === n2.type && n1.key JJJ=== n2.key
}while (i <= e1 && i <= e2) {const n1 = c1[i]const n2 = c2[i]if (isSomeVNodeType(n1, n2)) {patch(n1, n2,...)} else {break;}i++;}
}

预处理后置节点

function patchkeyChildren(c1,c2,...){
...
...
while(i <= e1 && i <= e2) {const n1 = c1[e1]const n2 = c2[e2]if (isSomeVNodeType(n1, n2)) {
patch(n1, n2,... )
} else {
break;
}
e1--;e2--;}
}

3.处理仅有新增节点情况 新节点比老节点多

function patchkeyChildren(c1,c2,...){
...
...
if (i > e1) {
if (i <= e2) {
while (i <= e2) {
patch(null, c2[i],...)
i++;
}
}
}

4.处理仅有卸载节点情况也就是老节点比新节点多

老节点 a b c
新节点 a b

function patchkeyChildren(c1,c2,...){
...
...
if(i > e2){if(i <= e1){while(i <= e1){unmount(c1[i].el)}}
}
}

⭐️⭐️⭐️5.处理其他情况(新增/卸载/移动)

创建新的 在老的里面不存在,在新的里面存在
删除老的 在老的里面存在,新的里面不存在
移动 节点存在于新的和老的节点,但是位置变了
实现删除功能

两种方法查找新节点到底存在于老节点 一种方法是遍历 ,另一种是Key,Key是节点的唯一标识 能提高效率 这也是Vue中为何总要写key属性
定义s1、s2变量 分别记录要处理部分的起始位置

...
else{
let s1 = i;//旧节点开始位置
let s2 = i;//新节点开始位置const keyToNewIndexMap = new Map()
//遍历新节点保存key映射表
for (let i = s2; i <= e2; i++) {const nextChild = c2[i]keyToNewIndexMap.set(nextChild.key, i)}
}
for(let i = s1; i <= e1; i++){const prevChild = c1[i]let newindex;if (prevChild.key != null) {newIndex = keyToNewIndexMap.get(prevChild.key)} else {for (let j = s2; j <= e2; j++) {if (isSomeVNodeType(prevChild, c2[j])) {newIndex = j;break;}}
}
//如果没有找到 则直接删除旧节点中元素
if (newIndex === undefined) {unMount(prevChild.el)}else{patch(prevChild, c2[newIndex], ...)
}
}

优化 中间部分老的比新的多 那么多出来的可以直接删掉

const toBePatched = e2 - s2 + 1;
let patched = 0;for (let i = s1; i <= e1; i++) {
...
if (patched >= toBePatched) {unMount(prevChild.el)continue;}//在patch后
...
patched++
移动实现

这里就需要借助最长递增子序列算法提高效率了 因为要移动位置 要频繁dom操作,效率很慢,可以筛选那些老节点和新节点都有递增有顺序的节点不动

//先建立映射关系
const newIndexToOldIndexMap = new Array(toBePatched)
for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
...
...
//在patch前实现
newIndexToOldIndexMap[newIndex - s2] = i + 1; //不能把值设为0 他是有特殊意义的patch(prevChild, c2[newIndex], container, ...)const increasingNewIndexSequence =getSequence(newIndexToOldIndexMap)
//指针
let j = 0
for(let i =0;i < toBePatched; i++){if(i !==increasingNewIndexSequence[j]){console.log("移动位置")}else{j++}
}

优化 调用最长递增子序列也会浪费一定性能 当 可以定义一个变量moved 如果移动再开始
没有移动则为false

let moved = false;
let maxNewIndexSoFar = 0;
...
if (newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex} else {moved = true}
...
const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []
if(moved){console.log('插入操作')
}
创建新的节点
if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild)
}
实现完成. 完整代码
function patchKeyedChildren(c1: any, c2: any, container, parentComponent, parentAnchor) {let i = 0let e1 = c1.length - 1;let e2 = c2.length - 1function isSomeVNodeType(n1, n2) {return n1.type === n2.type && n1.key === n2.key}// 左侧while (i <= e1 && i <= e2) {const n1 = c1[i]const n2 = c2[i]if (isSomeVNodeType(n1, n2)) {patch(n1, n2, container, parentComponent, parentAnchor)} else {break;}i++;}while (i <= e1 && i <= e2) {const n1 = c1[e1]const n2 = c2[e2]if (isSomeVNodeType(n1, n2)) {patch(n1, n2, container, parentComponent, parentAnchor)} else {break;}e1--;e2--;}if (i > e1) {if (i <= e2) {const nextPos = e2 + 1;const anchor = e2 + 1 < c2.length ? c2[nextPos].el : nullwhile (i <= e2) {patch(null, c2[i], container, parentComponent, anchor)i++;}}} else if (i > e2) {while (i <= e1) {
//删除操作
hostRemove(c1[i].el)i++}} else { // Array to Array 中间乱序let s1 = i;let s2 = i;const keyToNewIndexMap = new Map()for (let i = s2; i <= e2; i++) {const nextChild = c2[i]keyToNewIndexMap.set(nextChild.key, i)}const toBePatched = e2 - s2 + 1;let patched = 0;const newIndexToOldIndexMap = new Array(toBePatched)// 中间值发生改变再调用方法let moved = false;let maxNewIndexSoFar = 0for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0for (let i = s1; i <= e1; i++) {const prevChild = c1[i];if (patched >= toBePatched) {hostRemove(prevChild.el)continue;}let newIndex;if (prevChild.key != null) {newIndex = keyToNewIndexMap.get(prevChild.key)} else {for (let j = s2; j <= e2; j++) {if (isSomeVNodeType(prevChild, c2[j])) {newIndex = j;break;}}}if (newIndex === undefined) {hostRemove(prevChild.el)} else {if (newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex} else {moved = true}// 能代表新节点存在newIndexToOldIndexMap[newIndex - s2] = i + 1; //不能把值设为0 他是有特殊意义的patch(prevChild, c2[newIndex], container, parentComponent, null)patched++;}}const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []let j = increasingNewIndexSequence.length - 1;for (let i = toBePatched - 1; i >= 0; i--) {const nextIndex = i + s2;const nextChild = c2[nextIndex]const anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null;if (newIndexToOldIndexMap[i] === 0) {patch(null, nextChild, container, parentComponent, anchor)}if (moved) {if (j < 0 || i !== increasingNewIndexSequence[j]) {hostinsert(nextChild.el, container, anchor)} else {j--}}}}}
//递增子序列算法
function getSequence(arr: number[]): number[] {const p = arr.slice();const result = [0];let i, j, u, v, c;const len = arr.length;for (i = 0; i < len; i++) {const arrI = arr[i];if (arrI !== 0) {j = result[result.length - 1];if (arr[j] < arrI) {p[i] = j;result.push(i);continue;}u = 0;v = result.length - 1;while (u < v) {c = (u + v) >> 1;if (arr[result[c]] < arrI) {u = c + 1;} else {v = c;}}if (arrI < arr[result[u]]) {if (u > 0) {p[i] = result[u - 1];}result[u] = i;}}}u = result.length;v = result[u - 1];while (u-- > 0) {result[u] = v;v = p[v];}return result;}

这篇关于用通俗语言讲解 vue3中Diff算法完全解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1032646

相关文章

MySQL字符串转数值的方法全解析

《MySQL字符串转数值的方法全解析》在MySQL开发中,字符串与数值的转换是高频操作,本文从隐式转换原理、显式转换方法、典型场景案例、风险防控四个维度系统梳理,助您精准掌握这一核心技能,需要的朋友可... 目录一、隐式转换:自动但需警惕的&ld编程quo;双刃剑”二、显式转换:三大核心方法详解三、典型场景

Springboot3 ResponseEntity 完全使用案例

《Springboot3ResponseEntity完全使用案例》ResponseEntity是SpringBoot中控制HTTP响应的核心工具——它能让你精准定义响应状态码、响应头、响应体,相比... 目录Spring Boot 3 ResponseEntity 完全使用教程前置准备1. 项目基础依赖(M

SQL Server中行转列方法详细讲解

《SQLServer中行转列方法详细讲解》SQL行转列、列转行可以帮助我们更方便地处理数据,生成需要的报表和结果集,:本文主要介绍SQLServer中行转列方法的相关资料,需要的朋友可以参考下... 目录前言一、为什么需要行转列二、行转列的基本概念三、使用PIVOT运算符进行行转列1.创建示例数据表并插入数

C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解

《C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解》:本文主要介绍C++,C#,Rust,Go,Java,Python,JavaScript性能对比全面... 目录编程语言性能对比、核心优势与最佳使用场景性能对比表格C++C#RustGoJavapythonjav

SpringBoot返回文件让前端下载的几种方式

《SpringBoot返回文件让前端下载的几种方式》文章介绍了开发中文件下载的两种常见解决方案,并详细描述了通过后端进行下载的原理和步骤,包括一次性读取到内存和分块写入响应输出流两种方法,此外,还提供... 目录01 背景02 一次性读取到内存,通过响应输出流输出到前端02 将文件流通过循环写入到响应输出流

C语言逗号运算符和逗号表达式的使用小结

《C语言逗号运算符和逗号表达式的使用小结》本文详细介绍了C语言中的逗号运算符和逗号表达式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习... 在C语言中逗号“,”也是一种运算符,称为逗号运算符。 其功能是把两个表达式连接其一般形式为:表达

Go语言实现桥接模式

《Go语言实现桥接模式》桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立地变化,本文就来介绍一下了Go语言实现桥接模式,感兴趣的可以了解一下... 目录简介核心概念为什么使用桥接模式?应用场景案例分析步骤一:定义实现接口步骤二:创建具体实现类步骤三:定义抽象类步骤四:创建扩展抽象类步

GO语言实现串口简单通讯

《GO语言实现串口简单通讯》本文分享了使用Go语言进行串口通讯的实践过程,详细介绍了串口配置、数据发送与接收的代码实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 目录背景串口通讯代码代码块分解解析完整代码运行结果背景最近再学习 go 语言,在某宝用5块钱买了个

SpringBoot+Vue3整合SSE实现实时消息推送功能

《SpringBoot+Vue3整合SSE实现实时消息推送功能》在日常开发中,我们经常需要实现实时消息推送的功能,这篇文章将基于SpringBoot和Vue3来简单实现一个入门级的例子,下面小编就和大... 目录前言先大概介绍下SSE后端实现(SpringBoot)前端实现(vue3)1. 数据类型定义2.

SQL 注入攻击(SQL Injection)原理、利用方式与防御策略深度解析

《SQL注入攻击(SQLInjection)原理、利用方式与防御策略深度解析》本文将从SQL注入的基本原理、攻击方式、常见利用手法,到企业级防御方案进行全面讲解,以帮助开发者和安全人员更系统地理解... 目录一、前言二、SQL 注入攻击的基本概念三、SQL 注入常见类型分析1. 基于错误回显的注入(Erro