React 的虚拟 DOM 和 fiber

1,017 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 2 天,点击查看活动详情

这道题一般是面试 React 的必考题,其实主要也是检验你对 React 核心原理的理解程度,所以这道题就是个必会题!

React 用于构建用户界面的 JavaScript 库

React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。

上述这段话其实非常好理解,比如:大家经常在 React 里面用到的组件,如函数组件、类组件、Provider、Consumer 等,使用它们构建了虚拟 DOM。

虚拟 DOM

官方是如下这么说的,我们来进行一下解读(至于为什么放官方说明,因为官方才是正解啊!)。

什么是 Virtual DOM?

Virtual DOM 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调

解读:这段话描述的虚拟 DOM 就显得高大上了一点,直接是通过概念的层级来解释的。虚拟 DOM 其实表现到数据结构上就是一个 js 对象,也就是说用 js 对象来描述真实的 DOM 节点。然后页面肯定不是静态的,后面是要进行更新的,是先去更新 js 对象,就是所谓的虚拟 DOM,更新完 js 对象之后,再根据新的 js 对象,再同步到真实的 DOM 节点里就可以了,而同步的过程叫做协调。

这种方式赋予了 React 声明式的 API:您告诉 React 希望让 UI 是什么状态,React 就确保 DOM 匹配该状态。这使您可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。

与其将 “Virtual DOM” 视为一种技术,不如说它是一种模式,人们提到它时经常是要表达不同的东西。在 React 的世界里,术语 “Virtual DOM” 通常与 React 元素 关联在一起,因为它们都是代表了用户界面的对象。而 React 也使用一个名为 “fibers” 的内部对象来存放组件树的附加信息。上述二者也被认为是 React 中 “Virtual DOM” 实现的一部分。

通俗的讲:用 JS 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JS 对象结构。这个 JS 对象称为 Virtual DOM。

那接下来就会问为什么会用 js 对象来描述 DOM 节点呢?

以前用原生 js 开发、JQ 开发,都是直接使用 DOM,更新 DOM 的。这么用其实也没什么错误,JQ 曾经也是最伟大的框架嘛!但是如果需要更新的节点非常多,去更新的这个过程的当中,更新 DOM 节点,就意味着是跟用户直接交互,对于用户来说这个页面就好像有点闪了。

如果网再差一点,手机、电脑等设备性能再低一点,这个更新的过程会非常慢,是严重影响到用户体验的,怎么办?还想要达到更新的目的,那么换一个角度来说,可不可以去计算一下当前到底要更新哪些东西?计算一下,以一个最小的成本来更新这些节点,可不可以呢?

怎么去计算呢?所谓要更新的这些东西,可以把它放到一个 js 对象上来去进行比较,这个时候可以构建一个新老虚拟 DOM 对象,老的虚拟 DOM 对象描述对老的真实 DOM 节点,新的来描述新的 DOM 节点。中间比较的过程得出一个最小的差值,然后去渲染真实 DOM 到节点的时候,只更新这个差值,以最小的成本达到目的,这样的话对用户体验是比较好的。

总结:DOM 操作很慢,轻微的操作都可能导致页面重新排版,非常耗性能。相对于 DOM 对象,js 对象处理起来更快,而且更简单。通过 diff 算法对比新旧 vdom 之间的差异,可以批量的、最小化的执行 DOM 操作,从而提升用户体验。

React 是怎么用虚拟 DOM 的?

React 中是用 JSX 语法来描述视图的(View)。

React 不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息。

在此顺带上 JSX 的一些信息:

1、什么是 JSX?

语法糖
React 使用 JSX 代替常规的 JS
JSX 是一个看起来很像 XML 的 JS 语法扩展

2、为什么需要 JSX?

开发效率:使用 JSX 编译模板简单快速
执行效率:JSX 编译成 js 代码后进行了优化,执行更快
类型安全:在编译过程中就能发现错误

协调

上述提到了协调,所谓的协调过程,也就是新老 vdom 去进行一个对比的过程。对比完之后同步到真实的 DOM 节点上,协调的核心其实就在于 diff。上述已经说过,其实 vdom 本质的话是一个 js 对象,或者说给它描述成一个树状图,不管是 React 还是 Vue,都是这样的一个结构。

设计动机

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

此算法有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以使用 key 属性标识哪些子元素在不同的渲染中可能是不变的。

在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

diff 策略

1、(同层级)同级比较,Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

2、(同类型)拥有不同类型的两个组件会生成不同的树形结构。

3、(同key)开发者可以通过key 来暗示哪些子元素在渲染中能保持稳定。

diff 过程

对比两个虚拟 DOM 时会有三种操作:删除、替换、更新。

vnode 是现在的虚拟 DOM,newVnode 是新虚拟 DOM。

删除:newVnode 不存在时
替换:vnode 和 newVnode 的类型不同或者 key 不同
更新:有相同的类型和 key ,但 vnode 和 newVnode 不同

Shadow DOM 和 Virtual DOM 是一回事吗?

不,他们不一样。Shadow DOM 是一种浏览器技术,主要用于在 web 组件中封装变量和 CSS。Virtual DOM 则是一种由 Javascript 类库基于浏览器 API 实现的概念。

什么是 “React Fiber”?

Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。了解更多.

其实 fiber 说白了它还是虚拟 DOM, 就是一个 js 对象,它只是说原先的虚拟 DOM 在 fiber 出现之前,就 16.3 版本之前也是有虚拟 DOM,只不过说当时的虚拟 DOM 它的结构跟现在的 Vue 是一样的,就是说它的子节点就是一个数组,多个子节点的话就是个数组。数组的优点,可以通过下标快速去找到节点,但是要跳跃查找的话就麻烦了。React 刚开始的时候也没有考虑这个比较复杂的情形,但是随着 React 的进展,发现对于一些大型项目来说,组间树越大,要进行递归遍历,递归遍历成本肯定会特别高,但是 js 只有一个主线程,如果说被一直占着,后面来了优先级更高的任务就没法得到立即处理。

视觉上对用户来说,就是卡顿影响了用户体验。什么是优先级高的任务呢?举一个小小的例子:比如说现在有一个模糊查询输入框,现在要输入一些东西,输入这些东西之后,至少要发生两个任务,一个是输入框自身要更新,另一个是模糊查询数据展示也要进行更新,至少两个任务哪个优先级高,肯定是输入框优先级高,不能说下面模糊查询数据已经出来了,输入框没有更新。

所以这个时候是有一些高优先级的任务的,高优先级任务是应该被立即得到处理的。如果是一个数组,后面涉及到这种高优先级的这种任务的跳转就会比较麻烦。所以这个时候可以做一个拆分,如果是个数组不太方便,就改一下结构,比如给它整上链表结构,链表结构想指向哪里就指向哪里,这样就方便多了。

其实最终的目的就是做一个任务的分解,分成原先的一个虚拟 DOM。一大块没法去做跳跃怎么办?那就拆一下,拆成一个个的小虚拟 DOM 节点。起个名字叫fiber,最终的目的其实就是增量渲染,把渲染任务拆分一下,拆分成块匀到多帧上。只有把任务拆分的够细,才方便赋予优先级。

通俗的讲:fiber 是组件上已经完成或者将要完成的任务,每个组件可以有一个或者多个。

1、为什么需要 fiber?

对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用、结果就是主线程上的动画、布局等周期性的任务无法得到处理,造成视觉上的卡顿,影响用户体验。

2、任务分解的意义

解决上述的问题

3、增量渲染(把渲染任务拆分成块,匀到多帧)

4、更新时能够暂停、终止、复制渲染任务

5、给不同的类型更新赋予优先级

6、并发方面新的基础能力

7、更流畅