前端技能树,面试复习第 27 天—— React Diff 算法的原理,和 Vue 有什么区别 | 虚拟 DOM | key 的原理,为什么要用

1. 对虚拟 DOM 的理解?虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?

从本质上来说,Virtual Dom 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数减少修改 DOM 的重绘重排次数,提高渲染性能。

虚拟 DOM 是对 DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。它设计的最初目的,就是更好的跨平台,比如 node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 dom,因为虚拟 dom 本身是 js 对象。 在代码渲染到页面之前,vue 或者 react 会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟 dom 都会缓存一份,变化之时,现在的虚拟 dom 会与缓存的虚拟 dom 进行比较。在 vue 或者 reac t内部封装了diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

另外现代前端框架的一个基本要求就是无须手动操作 DOM,**一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,**另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。

为什么要用 Virtual DOM:

(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能

下面对比一下修改DOM时真实 DOM 操作和 Virtual DOM 的过程,来看一下它们重排重绘的性能消耗∶

  • 真实DOM∶ 生成 HTML 字符串+ 重建所有的 DOM 元素
  • Virtual DOM∶ 生成 vNode+ DOMDiff +必要的 DOM 更新

Virtual DOM 更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。

尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。

(2)跨平台

Virtual DOM 本质上是 JavaScript 的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp 等。

2. React diff 算法的原理是什么?

实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,**将更新补丁作用于真实 DOM,**以最小成本完成视图更新。

具体的流程如下:

  • 真实的 DOM 首先会映射为虚拟 DOM;
  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch(补丁),这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
  • 根据 patch 去更新真实的 DOM,反馈到用户的界面上。

一个简单的例子:

import React from 'react'
export default class ExampleComponent extends React.Component {
  render() {
    if(this.props.isVisible) {
       return <div className="visible">visbile</div>;
    }
     return <div className="hidden">hidden</div>;
  }
}

这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。

<div class="visible">visbile</div>

当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerTexthidden这样一个生成补丁、更新差异的过程统称为 diff 算法。

Diff算法的三个策略

diff算法可以总结为三个策略,分别从组件元素三个层面进行复杂度的优化:

  • 树: 只对同一层次的节点进行比较
  • 组件: class 一致,则默认为相似的树结构;不是则直接放入补丁中。只要父组件类型不同,就会被重新渲染。
  • 元素/节点:通过标记的 key 值进行对比。

策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)

这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。

策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)

在组件比对的过程中:

  • 如果组件是同一类型则进行树比对;
  • 如果不是则直接放入补丁中。

只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponentReact.memo 可以提高性能的原因。

策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)

元素比对主要发生在同层级中,通过标记节点操作生成补丁。**节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,**此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。

3. React key 是干嘛用的 为什么要加?key 主要是解决哪一类问题的

Key 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要 保证某个元素的 key 在其同级元素中具有唯一性`。

在 React Diff 算法中 React 会借助元素的 Key 值来 判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系

注意事项:

  • key值一定要和具体的元素一一对应;

  • 尽量不要用数组的 index 去作为 key;

  • **不要在 render 的时候用随机数或者其他操作给元素加上不稳定的 key,**这样造成的性能开销比不加 key 的情况下更糟糕。

  • 用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个生命周期中都保持稳定

  • **别用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

  • 千万别用随机数或者时间戳作为 key,不然旧节点会被全部删掉,新节点重新创建。

使用数组下标作为 v-for 的 key 值所带来的的问题

在用 v-for 更新已渲染的元素列表的时候,会使用就地复用的策略;
也就是说列表数据修改的时候,他根据key值去判断某个值是否改变了,如果改变了就重新渲染,不然就复用之前的元素v-for 可以使用数据本身所具有的唯一值作为 key,也可以使用索引 index 作为 key

eg. 当我们修改 list,向其中插入一条数据时: image.png
可见,除了 name 为 aa 的那条数据的 key 值没变外,另外两个都变了,也就是说 aa 可以复用, 而另外两条数据虽然值没变,但 key 值改变了,需要重新渲染,这种效率是很低的。这时我们可以使用 list 的 id 作为 key 来提高效率。

4. 虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么

虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。

在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。

5. React 与 Vue 的 diff 算法有何不同?

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁

React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法
但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型的比对,分别是树、组件及元素,以此提升效率。

  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。

以上是经典的 React diff 算法内容。

自 React 16,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNodeFiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。
整个更新过程currentworkInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。

Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力, 但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用 防抖和节流去提高响应性能。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程轨迹_

期望和你分享一杯咖啡

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

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

打赏作者

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

抵扣说明:

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

余额充值