深入理解React – 2

上节讲述了React中的JSX,实际上就是一种语法糖,最终是通过React的createElement转换为reactElement对象即虚拟DOM,再通过render方法转换为实际DOM的。

可以这样理解虚拟DOM:他是一个存储在内存里描述真实DOM结构的一棵树,使用的是javascript对象。他是React自己维护的轻量级结构快照。

React的策略是先在虚拟DOM上计算出最小的改动,再去真实DOM上施工。

那么问题来了,为什么要用虚拟DOM呢,为什么不直接转换成DOM呢?也就是说虚拟DOM有什么好处呢?

实际的好处有三点:

  1. 很多人认为DOM操作很慢,所以搞了虚拟DOM。 这句话不完全对,实际上DOM的操作本身不是慢,是“复杂 + 不可控”。 DOM的问题在于API多,浏览器差异,修改开销很大(直接更新DOM导致频繁的回流和重绘)
  2. React的核心思想是 UI = render(state), 所以在改变状态时必然要重新render,比较前后两次render的差异,再决定如何更新,因此有了虚拟DOM这个产物
  3. 虚拟DOM提供了批量更新和跨平台的能力,有了虚拟DOM就可以合并多次setState。 并且通过虚拟DOM这个中间产物可以做到分发到不同的平台上:
    • 浏览器(React DOM)
    • 原生(React Native)
    • Canvas WebGL
    • 终端设备

在更新实际DOM之前,React需要先对比虚拟DOM和实际DOM的区别,然后将有差异的DOM更新,而不是全量更新,这里的对比使用的就是diff算法。 下面简单介绍一下这个diff算法

React不做全量更新的前提是使用了三个工程化假设

  1. 只在同一层级进行比较,也即只要层级发生了变化,整个节点就被认为是有变化
  2. 不同的type 会被直接认为是有变化,会被直接替换。type指的是HTML标签,如div, span等
  3. key决定了节点的身份。 在循环输出时react要求必须给子节点一个key值,当key值发生变化时节点也被认为产生了变化。

总结: React 的虚拟 DOM 本质是用 JS 对象描述 UI,通过 Diff 算法计算最小更新,再批量同步到真实 DOM,从而实现声明式、可预测、跨平台的 UI 更新模型。

接下来我们详细看一下react从一次state更新到真实DOM发生变化内部到底发生了什么。

  1. setState -> dispatch
  2. 调度 -> scheduler
  3. Render阶段 (构建fiber + diff, 可中断)
  4. Commit阶段 (构建真实DOM, 不可中断)
  5. 浏览器绘制 (Paint)

下面我们具体看一下整个过程

1. 触发更新

我们知道触发react组件的更新是state的变化,在state变化之后React做的第一件事是创建一个update对象,挂载到当前Fiber的updateQueue上。 (Fiber是React内部的一种数据结构, 可以把它理解为一种可以恢复也可以打断的数据节点)

2. 进入调度阶段 scheduler

React会根据下面的信息来决定是否立即计算:

  • 更新来源
  • 当前是否在渲染
  • 优先级
3. 进入render阶段

从根节点开始循环处理Fiber工作单元。

注1: render阶段不会更改DOM
注2: Fiber不仅仅包括组件,也包括DOM标签, DOM节点,Fragment等

3.1 执行函数组件 (重新render)

在render阶段,react对每一个Fiber调用函数,执行hooks,生成新的React Element (虚拟DOM)

3.2 新旧虚拟DOM对比

这是构建fiber tree的前提,WIP Fiber tree就是通过对比虚拟节点和current Fiber tree逐步构建成的

3.3 给Fiber打副作用标记:(effect flags)

在这里给要变化的fiber打上需要如何操作的标记:

  1. placement -> 插入
  2. update -> 更新
  3. deletion -> 删除

3.4 从根节点开始构建workInProgress Fiber tree

current Fiber tree(屏幕上正在用的节点) -> workInProgress Fiber tree(正在构建的新节点)

一句话总结: React 的 render 阶段是一个遍历 Fiber 的过程:在遍历每个 Fiber 时,React 会执行组件函数生成新的 React Element,并立即在 reconcile 阶段将这些 Element 与 current Fiber 对齐,逐步构建 workInProgress Fiber 树并打上副作用标记。

4. Commit 阶段 (修改DOM)

此阶段不可中断

4.1 before mutation – 在修改DOM之前, react会先执行getSnapshotBeforeUpdate,创建一个快照,并保存当前DOM的状态 (如滚动位置等)

4.2 mutation – React会按照之前打上的标记来操作DOM,常见操作包括:

  • appendChild
  • removeChild
  • setAttribute
  • textContent = “”

这里是唯一会操作真实DOM的地方

4.3 DOM更新完成后,浏览器还未进行重绘

React会执行useLayoutEffect 这个hook

5. 浏览器渲染 (paint)

在这里浏览器进行绘制需要的操作:样式计算,布局,绘制等,用户看到了变化

React也会在浏览器渲染之后执行useEffect这个hook。

总结: React在state更新后会在内存中重新执行render,构建fiber树,并通过diff算法标记,合并变化计算出最小更新,再在commit阶段将这些更新同步到真实DOM上,最后交给浏览器渲染


Posted

in

by

Tags: