虚拟 DOM (VirtualDOM)的本质是什么?Diff 算法是如何工作的?为什么需要 key ?

直接操作真实 DOM 的开销是昂贵的,因为它会触发浏览器复杂的渲染流水线,包括重排(Reflow)和重绘(Repaint)。为了最大限度地减少对真实 DOM 的操作,React 引入了虚拟 DOM (Virtua DOM)的概念。

虎拟 DOM 的本质是一个轻量级的 JavaScript 对象,它是对真实 DOM 结构的一层抽象和描述。当组件状态发生变化时,React 并不直接修改真实 DOM,而是遵循一个“三步走”策略:

  • 1.在内存中根据新的状态,生成一棵新的虚拟 DOM 树。
  • 2.通过 Diff 算法,比较新旧两棵虚拟 DOM 树的差异。
  • 3.将计算出的最小化差异(patches),以批处理的方式,一次性地应用到真实 DOM 上。

React 的 Diff 算法为了实现高效对比,做出了几个关键的权衡和假设。它只进行同层级比较,并且只有当节点类型相同时才会继续深入比较。而对于同层级的一组子节点(例如列表),key 属性扮演了至关重要的角色。

key 是节点的唯一标识符。如果没有 key,当列表发生变化(如在开头插入一个元素)时,React 只能按位置逐一对比,这会错误地导致后续所有节点都被认为是“更新”了,从而进行大量不必要的 DOM 操作。而有了稳定且唯-的 key,React 就能精确地识别出哪些节点是新增的、哪些是删除的、哪些只是移动了位置。这使得 React 可以最大限度地复用已有的 DOM 元素,极大地提升了列表更新的性能。因此,key 的正确使用是 React 性能优化的一个核心实践。

Virtual DOM 的引入带来了以下好处

  1. 提高性能:
    Virtual DOM 通过将真实 DOM 的操作转换为对虚拟 DOM 的操作,然后批量更新到真实 DOM 中,减少了对真实 DOM 的频繁操作。这样可以提高页面渲染的性能,减少了浏览器重绘和重排的次数,提升了用户体验。
  • 减少 DOM 操作的次数:虚拟 DOM 可以将多次操作合并为一次操作,比如添加 1000 个节点,却是一个接一个操作的;
  • 减少 DOM 操作的范围:虚拟 DOM 借助 DOM diff 可以把多余的操作省掉,比如添加 1000 个节点,其实只有 10 个是新增的。
  1. 简化开发:
    使用 Virtual DOM 可以使开发人员更专注于应用程序的逻辑和数据,而不必过多地关注 DOM 操作的细节。通过使用组件化开发模式,开发人员可以将 UI 拆分成小而独立的组件,每个组件都有自己的状态和行为,这样可以更容易地管理和维护应用程序的代码。

  2. 跨平台兼容性:
    由于 Virtual DOM 是一个与平台无关的抽象层,因此可以轻松地将 React 应用程序移植到不同的平台上,包括 Web、移动端和桌面端等。这样可以提高应用程序的可移植性和跨平台兼容性。

  3. 方便的测试:
    使用 Virtual DOM 可以方便地进行单元测试和集成测试,因为可以在内存中操作虚拟 DOM,而无需真实的浏览器环境。这样可以加快测试速度,提高开发效率。

虚拟 DOM 长什么样子?

react:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vNode={
key:null,
props:{
children:[
{type:'span',...},
{type:'span',...},
],
className:'red',
onClick:()=>{}
},
ref:null,
type:'div',
...
}

vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vNode={
tag:"div",
data:{
class:'red',
on:{
click:()=>{}
}
},
children:[
{tag:'span',...},
{tag:'span',...},
],
...
}

实现虚拟 DOM 例子

  1. 定义虚拟 DOM 元素
  2. 创建文本节点
  3. 将虚拟 DOM 渲染为真实 DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 定义虚拟 DOM 元素
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
};

// 创建文本节点
const createTextElement = (text) => {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
};

// 将虚拟 DOM 渲染为真实 DOM
const render = (element, container) => {
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);

const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});

element.props.children.forEach((child) => render(child, dom));
container.appendChild(dom);
};

// React 的 createElement 和 render 函数使用
const MyReact = {
createElement,
render,
};

// 使用示例
const element = MyReact.createElement(
"div",
{ id: "container" },
MyReact.createElement("h1", null, "Hello, World!"),
MyReact.createElement("p", null, "This is a paragraph.")
);

const container = document.getElementById("root");
MyReact.render(element, container);

虚拟 DOM 的缺点

需要额外的创造函数,如 createElement 或 h,但可以通过 JSX 来简化 XML 写法,严重依赖打包工具

diff 算法

  1. 虚拟 DOM 树比较:在进行更新时,React 会将当前的虚拟 DOM 树与新的虚拟 DOM 树进行比较,找出差异。

  2. 差异检测:React 使用 深度优先搜索算法(DFS)对比两棵虚拟 DOM 树,找出两棵树之间的差异。它会逐个比较节点及其子节点,找出哪些节点需要更新、添加或删除。

  3. Diff 策略:React 使用了一些优化策略来减少 DOM 操作次数:

  • 同层比较:React 只会对比相同层级的节点,不会跨层级比较。
  • 唯一标识:在进行节点比较时,React 会使用节点的唯一标识(通常是 key 属性)来判断节点是否相同,从而避免不必要的更新操作。
  • 节点移动:React 会尽量复用已存在的节点,而不是重新创建新的节点。如果节点顺序改变,React 会尝试通过移动节点来达到更新效果,而不是直接删除和重新插入节点。
  • 批量更新:React 会将多个更新操作合并成一个批量更新,然后一次性更新到真实 DOM 中,以减少页面重绘和重排的次数。
  1. 应用差异:一旦找到了两棵虚拟 DOM 树之间的差异,React 就会根据这些差异来进行相应的操作,更新真实 DOM。

vue 中的 diff 算法在对新旧虚拟 dom 对比时,是从节点的两侧向中间对比,如果节点的 key 值和元素类型相同,属性值不同,就认为是不同节点,会将该节点删除重建
react 中的 diff 算法在对新旧虚拟 dom 对比时,是从节点的左边向右边对比,如果节点的 key 值和元素类型相同,属性值不同,就认为是节点同类型,只修改当前节点的属性

逻辑

  1. Tree diff
  • 将新旧两棵树逐层对比,找出哪些节点需要更新
  • 如果节点是组件就看 Component diff
  • 如果节点是标签就看 Element diff
  1. Component diff
  • 如果节点是组件,就先看组件类型
  • 类型不同直接替换(删除旧的)
  • 类型相同则只更新属性
  • 然后深入组件做 Tree diff(递归)
  1. Element diff
  • 如果节点是原生标签,则看标签名
  • 标签名不同直接替换,相同则只更新属性
  • 然后进入标签后代做 Tree diff(递归)

dom diff 有什么缺点

同级比较,存在 BUG,需要加个 唯一 Key,不能用 index 做下标

Fiber

Fiber 是 React v16 中引入的一种新的协调算法,用于调度和管理 React 的渲染和更新过程。Fiber 的目标是使 React 应用程序更加流畅和响应,减少页面卡顿和掉帧的情况。
Fiber 的核心思想是将 React 的渲染过程分解为可中断的小任务,并且可以根据优先级来调度这些任务的执行顺序。这样可以使 React 在执行渲染任务时更加灵活,可以根据页面的需要来调整任务的优先级和执行顺序,从而提高用户体验。

Fiber 的引入使得 React 具备了更高的并发能力和更灵活的调度策略,使得 React 应用程序可以更好地适应不同的场景和用户需求。

综上所述,Virtual DOM 和 Fiber 是 React 内部机制中的两个关键概念,它们共同作用于 React 的渲染和更新过程中,提高了 React 应用程序的性能、效率和响应能力。Virtual DOM 优化了对真实 DOM 的操作,而 Fiber 则优化了 React 的渲染和更新调度过程。

关于 DOM 的谣言:DOM 操作慢?虚拟 DOM 快

这句话类似于:刘翔矮(对比于姚明)
DOM 操作慢是对比于 JS 原生 API,如数组操作
任何基于 DOM 的库(Vue/React)都不可能再操作 DOM 时比 DOM 快

为什么网上有这样的谣言?

因为在某些情况下,虚拟 DOM 快

规模太大的时候,比如 100000 个节点,原生 DOM 要快,因为虚拟 DOM 要做大量的计算,规模小的时候(1000 个节点),虚拟 DOM 快

在测试插入 10000 个节点的时候,vue 要比 react 快很多,vue 的速度接近原生 JS,当然这两个都没有做任何优化