【Vue】虚拟 DOM 和 diff 算法

虚拟 DOM

虚拟 DOM 是一个抽象的 DOM 树副本,通常是一个纯 JavaScript 对象(本质上就是一个普通的 JS 对象),用于描述视图的页面结构。

可以在 mounted 中使用 this._vnode 看到该组件生成的虚拟 dom。

在这里插入图片描述

每一个组件都有一个 render 函数,每个 render 函数都会通过 h 函数创建并 返回一个 虚拟 DOM 树,所以每个组件都对应一个虚拟 DOM 树。

在这里插入图片描述

在这里插入图片描述

为什么需要虚拟 DOM?

在 vue 中,渲染视图会调用 render 函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时。如果在渲染时,直接使用真实 dom,由于真实 dom 的创建、更新、插入等操作会带来大量的性能损耗,极大降低渲染效率。虚拟 dom 主要解决渲染效率问题。

所以一个是创建一个个的普通对象(let obj = {} ),一个是创建一个个的 html 元素(let obj = document.createElement('div')),明显创建对象的效率更高。

虚拟 dom 转为 真实 dom?

一个组价实例首次被渲染时,先生成虚拟 dom 树,然后根据虚拟dom 树创建真实dom,并把真实 dom 挂载到页面上,每个虚拟 dom 都会对应一个真实 dom。

一个组件受响应书数据变化的影响,需要重新渲染时,会重新调用render 函数,创建一个新的虚拟 dom 树,然后新树和旧树进行对比,找到差异(需要更新的地方),使用新树,抛弃旧树,更新需要更新的真实 dom。

在这里插入图片描述

template 模板与虚拟 dom 的关系

vue 框架(主文件夹)中有一个 compile 模块(子文件夹),它主要负责将模板转换为 render 函数,而 render 函数调用后将得到虚拟 dom。

编译的过程分两步:

  1. 将模板(template,实际上转换后就是一个字符串)字符串转换成为 AST
  2. 将 AST 转换为 render 函数

AST类似于虚拟DOM,但仅仅是类似:
在这里插入图片描述
https://astexplorer.net/ 可用该网站查看代码对应的抽象语法树 AST

如果使用传统的引入方式(script)或 vue-cli 的配置(vue.config.js)中开启了runtimeCompiler:true,则编译时间发生在组件第一次加载时,这称之为运行时编译。

如果是在 vue-cli 的默认配置下,编译发生在打包时,这称之为模板预编译。这是运行时不需要编译,效率提高。

其实,不只是运行 build 的时候会打包,运行 npm run serve 等的时候也会打包,只是不形成打包文件。

编译是一个极其耗费性能的操作,预编译可以有效的提高运行时的性能,而且,由于运行的时候已不需要编译,vue-cli在打包时会排除掉vue中的compile模块,以减少打包体积。

模板的存在,仅仅是为了让开发人员更加方便的书写界面代码。

vue最终运行的时候,最终需要的是render函数,而不是模板,因此,模板中的各种语法,在虚拟dom中都是不存在的,它们都会变成虚拟dom的相关配置。

虚拟 DOM 的优势

  1. 性能优化:虚拟 DOM 可以减少直接操作实际 DOM 的次数,从而提高页面渲染性能。通过批量处理 DOM 更新操作,可以减少浏览器的重排和重绘。

  2. 简化开发:虚拟 DOM 提供了一种更抽象的方式来操作页面结构,使得前端开发更加容易理解和维护。它将组件化开发推向了前台,有助于构建模块化的应用。

  3. 更好的可维护性:虚拟 DOM 可以让开发者专注于数据和组件的状态,而不必担心手动管理实际 DOM 的状态。这提高了代码的可维护性。

diff 算法

Vue 中的 diff 算法是⽤于更新 Virtual DOM 树,从⽽实现⾼效的 DOM 操作。diff 算法会对⽐新旧两棵 Virtual DOM 树的差异,然后只更新必要的部分,从⽽减少 DOM 操作的次数。

在 diff 算法中,由于只更新必要的部分,所以可以⼤⼤提⾼ DOM 操作的效率。这也是 Vue 可以实现⾼效渲染的重要原因之⼀。

diff 算法包括以下几个步骤:

  • 新旧节点的⽐较,会⾸先⽐较新旧节点是否相同,如果相同,则继续⽐较⼦节点;如果不同,则进⾏下⼀步操作。
  • 对⼦节点进⾏⽐较 对新旧节点的⼦节点进⾏⽐较,具体分为以下四种情况:
    • 新节点没有⼦节点,旧节点有⼦节点:直接删除旧节点的⼦节点
    • 旧节点没有⼦节点,新节点有⼦节点:直接添加新节点的⼦节点
    • 新旧节点都有⼦节点:继续⽐较⼦节点
    • 新旧节点都有相同的⼦节点:对相同的⼦节点进⾏递归⽐较
  • 对旧节点多余的⼦节点进⾏删除。如果旧节点的⼦节点⽐新节点的⼦节点多,那么对于多余的⼦节点,直接进⾏删除。

下面是 Diff 算法的⼏个步骤:

  1. 比较两棵虚拟 DOM 树:Diff 算法会递归遍历两棵虚拟 DOM 树,一般来说,这两棵树是前后两次渲染的虚拟 DOM。算法会比较节点和节点之间的差异,包括节点的类型、属性和子节点。

  2. 找出节点差异:在比较过程中,Diff 算法会找出以下几种类型的差异:

    • 节点类型不同:如果节点类型不同,说明需要替换该节点。
    • 节点属性不同:如果节点的属性不同,需要更新实际 DOM 上的属性。
    • 节点顺序不同:如果子节点的顺序不同,需要重新排列实际 DOM 上的子节点。
    • 节点内容不同:如果节点的文本内容不同,需要更新实际 DOM 上的文本内容。
  3. 生成差异列表:Diff 算法将找到的差异记录在一个差异列表中,这个列表描述了需要对实际 DOM 进行哪些操作来使其与新的虚拟 DOM 一致。

  4. 批量更新实际 DOM:最后,Diff 算法会根据差异列表中的信息,批量更新实际 DOM。这可以通过最小化 DOM 操作来提高性能,因为 DOM 操作通常是较昂贵的操作。

Diff 算法的优化策略

为了提高 Diff 算法的效率,一些优化策略被引入:

  1. 基于节点的唯一标识:为每个节点分配唯一的标识符(key),这有助于算法在两次渲染中快速定位相同的节点,而不必遍历整棵树。

  2. 提前中断遍历:一旦算法发现某个子树完全一致,就可以提前中断对该子树的遍历,从而减少不必要的比较。

  3. 批量更新:Diff 算法不立即更新实际 DOM,而是将差异记录在队列中,然后一次性更新。这减少了浏览器的重排和重绘。

  4. 同级比较:在进行比较时,算法会首先比较同级节点,而不是跨级比较。这有助于提高效率。


diff 总结:

没有 key:

  1. 新的 VNode 依次替换掉旧的 VNode(重复的也要替换,浪费性能)
  2. 新增
  3. 删除

有 key(对组件数组节点的复用,对 diff 的优化):

  1. 前序算法比较结点(type:结点类型;key),相同则比较下一个,不相同则进行尾序对比
  2. 前序对比算法
  3. 后序对比算法(相当于头和头比较,尾和尾比较;不同于 Vue2 的双端 diff 算法还有头和尾,尾和头的交叉对比;因为 Vue3 用最长递增子序列算法进行了优化)
  4. 新节点多出来就挂载
  5. 旧节点多出来就卸载
  6. 特殊情况乱序处理
    a. 构建新节点的映射关系(key: index)
    b. 遍历旧节点,找到对应的新节点,执行 patch 和 unmount
    c. 监测节点是否移动,并生成最长递增子序列算法
    d. 移动和新增节点,确保 DOM 数的顺序和结构正确

为什么不建议用index作为key?
在组件数组节点移动的时候,比如尾部移动到头部,会导致所有元素index都变化(也就是说 index 是随着遍历而改变的,使用后端的 id 作为 key 才能做到更好使组件唯一)


特殊情况解释:

这个过程就像整理一副打乱顺序的卡片组,首先你找出哪些卡片是新的,哪些是多余的,然后你有计划地去匹配、删除、移动或新增卡片,尽量减少不必要的移动和操作,从而让整个过程既高效又省力。

这样一来,Vue 3 的这个 diff 算法就能确保页面更新时尽可能快、尽可能少地操作 DOM(网页的元素)。

  1. 构建新节点的快速查找表:

    • 想象你有一张新节点的清单(比如一组卡片),有些卡片上有独特的标记(key),就像每张卡片都有自己的编号。
    • 现在你做了一件聪明的事——你创建了一个快速查找表,把这些卡片的编号和它们在清单中的位置一一对应起来。这样,你就可以很快找到新卡片在清单中的位置,而不需要每次都从头找起。
  2. 遍历旧节点列表,寻找匹配:

    • 然后你开始查看旧的卡片,并试图在新卡片清单中找到对应的卡片。如果旧卡片有编号(key),你可以直接通过刚刚创建的查找表快速找到对应的新卡片。
    • 如果旧卡片没有编号,你只能通过比较卡片的内容去一个一个找匹配的卡片。
    • 如果找不到匹配的卡片,就说明这张旧卡片不需要了,于是你把它丢掉。
  3. 记录卡片位置,检查是否需要移动:

    • 你继续查看卡片,找到了一些匹配的卡片,但你会发现,有些卡片的位置发生了变化,比如原来在第二个位置的卡片,现在变到了第四个位置。
    • 为了追踪这种位置变化,你记录下每张新卡片现在的位置,并判断它是不是在正确的顺序上。如果你发现卡片的位置乱了,就标记下来,后面我们会去调整它们。
  4. 移动和新增卡片:

    • 最后一步,如果有些卡片位置错了,或者有新的卡片需要加进来,你就开始行动了。你会根据一个特殊的规则(“最长递增子序列”)找出哪些卡片已经在正确的位置上,这样你就只需要移动那些真的错了的卡片,而不是每张都动。
    • 同时,对于新的卡片,你也把它们插入到正确的位置上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值