[vue3] vue3更新组件流程与diff算法

Vue3中,组件的更新通过patch函数进行处理。

patch函数

源码位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,namespace = undefined,slotScopeIds = null,optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {// 二者相同,不需要更新if (n1 === n2) {return}// vnode类型不同,直接卸载旧节点if (n1 && !isSameVNodeType(n1, n2)) {anchor = getNextHostNode(n1)unmount(n1, parentComponent, parentSuspense, true)n1 = null}// ......const { type, ref, shapeFlag } = n2switch (type) {case Text:// 处理文字节点breakcase Comment:// 处理注释节点breakcase Static:// 静态节点breakcase Fragment:// Fragment节点breakdefault:if (shapeFlag & ShapeFlags.ELEMENT) {// 处理普通DOM元素} else if (shapeFlag & ShapeFlags.COMPONENT) {// 处理组件} else if (shapeFlag & ShapeFlags.TELEPORT) {// 处理teleport} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {// 处理suspense} else if (__DEV__) {// 报错:vnode类型不在可识别范围内warn('Invalid VNode type:', type, `(${typeof type})`)}}
}

patch函数用来挂载或者更新vnode

patch的大致流程:

  1. n1n2如果相等,则表示无变化,直接退出;
  2. n1n2如果引用不同,则先检查其vnode类型,如果类型不同,则直接卸载n1,挂载n2
  3. 主流程:根据n1n2的vnode类型,调用不同的process函数。

process

process函数的参数列表大致相同,都是要传入n1n2container等参数。patch函数主要起到一个分类讨论的功能。

这里只讨论普通元素类型组件类型的vnode处理过程,因为这是Vue应用中最常见、覆盖范围最广的两种类型。

普通元素类型,即ShapeFlags.ELEMENT,在浏览器环境下就是指DOM类型。

普通元素vs组件

从组件树的角度来理解普通元素和组件元素的区别。

image-20240807201257321

一个组件的children可以是普通元素或组件元素。

  • 叶子节点必须是普通元素,因为只有普通元素能够通过相关平台挂载到界面上。Vue会在编译时确定mount方法,以适应不同的平台。对于浏览器环境来说,普通元素是通过vnode来表示DOM节点,将vnode转换成实际DOM元素并插入到页面上的操作由vue3源码中的runtime-dom这个package实现。

    叶子节点必须是普通元素,但是普通元素不一定是叶子节点,比如一个div标签内部可以包含其它组件。

  • 叶子节点不可能是组件,因为组件必须被实现且被注册,其实现必须使用已注册的组件或者普通元素。并且组件是虚拟元素,并不能被实际挂载到指定平台上,只能递归地patch它的children,直到把普通元素都挂载到界面上。

在 Vue3 - patch 函数的源码中可以看到除了这两种类型,还有很多针对其它类型 vnode 的 process 函数,这些 process 函数主要做的只有两件事:挂载和更新。

对于旧vnode n1 和 新vnode n2

  • n1null时,则表示挂载n2
  • n1不为null时,则表示n1更新为n2

patchElement

patchElement 会对它的 children 也进行 patch,也就是调用 patchChildren 函数。

children 有三种情况:文本、数组、NULL。

image-20240821015027093

diff算法

diff 算法用于将旧children的vnode数组更新为新children的vnode数组,它通过比较两个序列,尽可能地复用相同的vnode,以此来减少频繁创建vnode带来的开销。

事实上,diff 是在 patchKeyedChildren 中实现的,对于没有设置 key 的数组,patchChildren函数内部调用的是 patchUnkeyedChildren,函数实现大致如下:

  1. 计算两个数组的长度的最小值 commonLength
  2. commonLength 个 vnode 直接 patch 更新,不会考虑移动到不同位置来复用;
  3. 旧序列如果有剩余则unmount,新序列如果有多余则mount。

这种做法在大多数情况下都会需要创建 vnode,开销还是比较大的。因此为了提高渲染性能,使用渲染列表的时候要写上 key。

image-20240821124406433

vue3 的 diff算法实现在patchKeyedChildren函数中,主要包含五个流程,其中第五个是最复杂的步骤:

  1. 两个序列从头部向尾部依次同步,直到不能匹配进入下个流程;

    起始索引都是从0开始,用一个变量 i 就可以了。

    image-20240821153449683
  2. 两个序列从尾部向头部依次同步,直到不能匹配进入下个流程;

    两个序列长度可能不一样,最后一个元素的索引不一样,因此需要两个变量 e1e2 来指向 ending index

    image-20240821153834805

在上述两个流程之后:

  1. 如果旧序列遍历完了,而新序列还有剩余,则新序列剩余的vnode依次mount;

    • i>e1则表示旧序列遍历完了;
    • i<=e2则表示新序列还有剩余;
    • while(i<=e2){...; i++}把剩余的vnode都挂载。
    image-20240821154722336
  2. 如果新序列遍历完了,而旧序列还有剩余,则旧序列剩余的vnode依次unmount;

    • i>e2表示新序列都遍历完了;
    • i<=e1表示旧序列还有剩余;
    • while(i<=e1){ unmount(...); i++ }将剩余的旧 vnode 都卸载。
    image-20240821170437767
  3. 未知序列,尽可能地通过移动复用vnode,剩下的mount或者unmount。

    头部和尾部都同步了若干vnode,但是两个序列都还没有遍历完成,说明中间有一段序列是混乱的、难以匹配的。

    image-20240821172910666

    在步骤5中,有细分为多个子步骤:

    首先用s1s2表示旧新序列的起始索引:const s1 = s2 = i;

    5.1. 遍历新序列,使用Map建立keynewIndex的映射:keyToNewIndexMap

    使用Map的原因是PropertyKey这个类型是联合类型string | number | symbol,不能简单的用对象或数组表示。

    建立 key 到 index 的映射,是为了后续我们可以通过旧序列中的 key 来建立可复用情况下新旧节点之间的映射关系。

    // 这段代码不是源码,只保留主干。
    const keyToNewIndexMap: Map<PropertyKey, number> = new Map()
    for (i = s2; i <= e2; i++) {const nextChild = c2[i];	// c2 即 children2if (nextChild.key != null) keyToNewIndexMap.set(nextChild.key, i)}
    }
    

    5.2. 遍历旧序列,使用一个newIndexToOldIndexMap数组建立新旧序列中可复用节点的位置对应关系。

    newIndexToOldIndexMap的作用

    通过这个数组,我们可以知道一个新vnode可以由哪个旧vnode更新得到。在这个数组中,newIndex是以 0 开始的,而 oldIndex 是以 1 开始的,这是为了把 oldIndex==0 作为一个特殊标识,表示新节点在旧序列中不存在。当newIndexToOldIndexMap[k] = 0,则表示新序列中第 k 个vnode在旧序列中不存在,无法复用。

    image-20240821202906314

    newIndexToOldIndexMap的构建过程

    遍历旧序列 c1

    for(let i=s1; i<=e1; i++){...}
    
    • 使用 keyToNewIndexMap 查询 c1[i] 的 key:

      • 如果 key 为 undefined,则说明这个旧的 vnode 在新序列中不存在了,卸载这个旧 vnode;

      • 如果 key 为某个数字,则表明这个旧 vnode 在新序列中有 vnode 的 key 跟它一样,可以复用。使用 patch 函数将旧节点更新为新节点。

        这一步骤中记录新旧序列索引映射的代码是 newIndexToOldIndexMap[newIndex - s2] = i + 1

        • 减去s2是因为序列包含 diff 算法步骤1同步的头节点;
        • i+1是因为这个数组记录的 oldIndex 是从 1 开始的。
    • 如果发现新序列中的节点都找到与之对应的旧节点了,那么 for 循环后续的旧节点都直接卸载。

    在这个步骤中,还通过一个 moved 变量来记录节点的相对位置是否被移动了:

    if (newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex
    } else {moved = true
    }
    

    如果当前新节点的索引 newIndex 大于或等于此前遍历到的最大新节点索引 maxNewIndexSoFar,那么当前节点在新列表中的顺序相对于旧列表来说是保持递增的。

    image-20240821211605198

​ 5.3 移动与挂载

​ 经过上面若干步骤,能复用的旧节点都通过 patch 将数据更新到新节点上了,不能复用的旧节点都被卸载了。

​ 而新节点如果没有在旧序列中出现,则挂载;如果在旧序列中出现了,

  • 如果moved为 false,则表示(已经执行复用操作的)新节点的顺序和在旧序列中的相对顺序是一致的,这种情况无需处理;

  • 如果moved为 true,则表示相对顺序不一致,需要移动 vnode。

    为了减少移动次数,这里应用了最长递增子序列算法,计算了数组newIndexToOldIndexMap的最长递增子序列。

    从上图右边的子图中可以看出,递增子序列越长意味着相对顺序一致的子序列越长,那么需要移动的 vnode 就越少。

    思考版图大致如下:

    image-20240821232108564

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

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

相关文章

DDD的函数式编程实现

DDD是一种成熟的软件设计方法,旨在确保领域专家和开发人员能够有效合作,创造出高质量的软件。 本文介绍咋将FP(函数式编程)应用于DDD的实现,使其既优雅又简洁。C4模型中,软件架构图分为四个层次:“系统上下文”、“容器”、“组件”和“代码”。 “组件”是构成容器的基…

使用FModel提取黑神话悟空的资产

介绍使用FModel提取黑神话悟空资产的方法目录前言设置效果展示闲聊可能遇到的问题没有相应的UE引擎版本选项 前言 黑神话悟空昨天上线了,解个包looklook。本文内容比较简洁,仅介绍解包黑神话所需的专项配置,关于FModel的基础使用流程,请见《使用FModel提取UE4/5游戏资产》 …

rust库-ouroboros中文文档

文档原文:https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html 属性宏ouroboros::self_referencing #[self_referencing]此宏用于将常规结构转换为自引用结构。举个例子: use ouroboros::self_referencing;#[self_referencing] struct MyStruct {int_dat…

mini-lsm通关笔记Week1Day4

项目地址:https://github.com/skyzh/mini-lsm 个人实现地址:https://gitee.com/cnyuyang/mini-lsmTask 1-SST Builder在此任务中,您需要修改: src/table/builder.rs src/table.rs SST由存储在磁盘上的数据块和索引块组成。通常,数据块都是懒加载的-直到用户发出请求,它们…

高效流程办公,相信自定义流程表单开发

对于自定义流程表单开发的优势特点,可以在本文中获得详细答案。如果要将企业内部的数据做好高效管理,需要借助更优质的软件平台。低代码技术平台够灵活、更高效、易维护、可视化操作等,可以满足日益扩大的业务需求,助力企业做好数据资源管理,共同为实现流程化办公和数字化…

041、Vue3+TypeScript基础,使用pinia库来储存数据

01、输入npm install pinia 02、main.js代码如下:// 引入createApp用于创建Vue实例 import {createApp} from vue // 引入App.vue根组件 import App from ./App.vue//第一步:引入pinia import {createPinia} from piniaconst app = createApp(App);//第二步:创建pinia实例 c…

三维几何生成:多段线、圆弧

一、三维空间多段线几何 1 应用背景 ​​  opengl常用glLineWidth命令设置线宽,此线宽在透视投影中不会随着相机远近变化而缩放。项目中高版本glLineWidth命令失效,需要考虑如何快速、方便、宽度不变的多段线几何。方案a:纯shader绘制曲线,绘制到一个二维平面上,然后将平…

本地快速安装运行史上最强开源LLaMa3大模型

https://liaoxuefeng.com/blogs/all/2024-05-06-llama3/史上最强开源AI大模型——Meta的LLaMa3一经发布,各项指标全面逼近GPT-4。它提供了8B和70B两个版本,8B版本最低仅需4G显存即可运行,可以说是迄今为止能在本地运行的最强LLM。 虽然LLaMa3对中文支持不算好,但HuggingFac…

第二章 redis环境安装与配置

redis环境安装 redis的官方只提供了linux版本的redis,window系统的redis是微软团队根据官方的linux版本高仿的。 官方原版: https://redis.io/ 中文官网:http://www.redis.cn 1、下载和安装 下载地址:https://github.com/tporadowski/redis/releases使用以下命令启动redis服务…

CVSS(Common Vulnerability Scoring System)打分规则解读

CVSS(Common Vulnerability Scoring System)提供了一种根据漏洞的主要特征进行打分,反映其严重性的方法。CVSS 已成为被广泛使用的标准。 下面是CVSS 3.1版本计算器的界面截图,本文对Base Score的打分标准做解读,并提供一些建议。同时会列出每个维度选项的翻译。 Attack V…

插入排序详细解读

插入排序详细解读 图解 第一轮:从第二位置的 6 开始比较,比前面 7 小,交换位置。第二轮:第三位置的 9 比前一位置的 7 大,无需交换位置。第三轮:第四位置的 3 比前一位置的 9 小交换位置,依次往前比较。第四轮:第五位置的 1 比前一位置的 9 小,交换位置,再依次往前比…

一张图看懂SAP主要流程

一张图看懂SAP主要流程