> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/react-jin-jie-shi-jian-zhi-nan/20.-jia-gou-pian-shu-ju-geng-xin-liu-cheng-she-ji.md).

# 20. 🔥架构篇-数据更新流程设计

前面我们讲到了 React 位运算的三种场景，提到了 Lane 模型，更新上下文 Context，接下来我们还是以 React 数据更新为主线，看一下数据更新的架构设计。

### 一 React 更新前置设计

**批量更新-减少更新次数**

虽然 JS 执行是快速的，但是浏览器绘制的成本却是昂贵的，所以良好的性能保障是：

1 减少更新次数，从而减少浏览器的渲染绘制操作，比如重绘，回流等。 2 避免 JS 的执行，影响到浏览器的绘制。

我们都知道 React 也是采用数据驱动的，所以当每一次触发 setState 或者是 useState 更新 state 的时候，本质上都是数据变化-> DOM 元素变化 -> 浏览器绘制，那么正常情况下，如果一次用户交互事件，比如点击事件中，可能会触发多次更新，接下来就会多次更改 DOM 状态，进而占用浏览器大量的时间，所以为了避免这种情况发生，React 通过更新上下文的方式，来判断每一次更新是在什么上下文环境下，比如在 React 事件系统中，就有 EventContext。在这些上下文中的更新，都是 React 可控的，进而可以批量处理这些更新任务。

这种批量更新的方式，一定程度上减少了更新次数，但是这种控制手段也仅仅只能对同一上下文中的更新生效，打个比方，一些微任务中的更新，这种更新就不受 React 更新上下文的控制了，这样浏览器还是需要处理一个更新之后，马上执行下一个任务，如果有很多这样的任务，就会导致一直执行 JS 线程，从而阻塞了渲染线程的绘制。

**更新调度-更新由浏览器控制**

还好 React 中一个重要的模块去处理更新，那就是——Scheduler，在 React 中维护了一个更新队列中，去保存这些更新任务，当第一次产生更新的时候，会先把当前更新任务放入到更新队列中，然后先执行更新，接下来调度会向浏览器请求空闲时间，在此期间，如果有其他更新任务插入，比如上述的微任务，就会放入更新队列中，如果浏览器空闲了，就会判断更新队列中是否还有待更新的任务，如果有那么执行，接下来再向浏览器请求下一个空闲帧，一直到待更新队列中没有更新任务，这样就保证了更新任务导致浏览器卡住的情况发生，把更新的主动权交给了浏览器。

有了批量更新和更新调度，就解决了上面的两种性能保障问题，不过问题又来了，那就是更新任务，并不是相同的，而是有不同优先级的任务，就像一条业务线的产品，在给研发提需求的时候，本质上每一个需求的优先级是不同的，有一些需求是高优先级，有一些就不是那么重要，如果一视同仁的处理这些需求，就不是很合理。

这个时候就需要把这些任务做一些区别，那满足一些复杂的场景。

**更新标识 Lane 和 ExpirationTime**

为了区别更新任务，每一次更新都会创建一个 update，并把 update 赋予一个更新标识，在之前的老版本中用的是 ExpirationTime ，但是在新版本 React 中，用的是 Lane，至于有两者的区别呢。

老版本 ExpirationTime 代表的是过期时间，当每次执行的任务的时候，会通过 ExpirationTime 来计算当前任务是否过期，如果过期了说明需要马上优先执行，如果没有过期，那么就让更高优先级的任务先执行，这就好比如上产品会把每一个需求增加了一个 deadline （过期时间），来确保需求的迫切性。

如果说把每次事件中产生的任务都公平对待的话，ExpirationTime 就不会出现什么问题，但是 concurrent 模式下有一个并发场景，比如我们通过一个输入框，来进行搜索数据并展示列表，那么本质上是产生了两个更新任务，一个是表单内容的变化，另外一个列表的展示，表单变化是急迫的任务，但是列表的展示相比表单内容显得不是那么重要。这个时候如果两个更新任务继续合并，那么最终会导致因为表单输入是频繁的，但是需要列表更新才能返回更新的内容，列表的更新会影响到表单的输入，反映到用户眼中的就是，输入内容的延时。这个时候就需要把表单内容更新和列表的更新当成两个任务去处理。

这个时候一个 ExpirationTime 并不能描述出当前 fiber 上有两个不同优先级的任务。ExpirationTime 只能反映出更新的时间节点，无法处理任务交割的场景。

所以就采用了另外一个模式， 那就是 Lane 模型，Lane 采用位运算的方式，一个 Lane 上可以有多个任务合并，这样能够描述出一个 fiber 节点上，存在多个更新任务，那么就可以优先处理高优先级任务，我们还是列举上面产品需求例子，在 Lane 模式下，每个需求给设置 P0，P1 等不同的等级，这样就保证了需求进行的有序性。

**进入更新**

有了更新标识和 update 之后，就可以更新了吗，显然不能，因为众所周知，整个 React 应用中会有很多 fiber 节点，而函数组件和类组件本质上也是 fiber ，和其他 fiber 不同的是组件可以触发更新，这个更新标识描绘出 React 的更新时机和更新特点。

在前面的章节中，我们讲到了 React 每一个更新都是从根节点开始向下调和，在此期间，会把双缓冲树交替使用作为最新的渲染 fiber 树。那么在构建最新 fiber 树的时候，没有发生更新的地方是不需要处理的，那么直接跳过更新就可以了，这也是一种性能上的优化，那么 React 首先要做的事情就是根据更新标识找到发生更新的源头，但是在众多 fiber 中如何快速找到更新源呢？这还是在标记更新标识的时候，会通过当前 fiber 的 return 属性更新父级 fiber 链上的属性 childLanes，这样在从 root 开始向下调和的时候，就能够直接通过这个属性找到发生更新的组件对应的 fiber，接下来执行更新。

### 二 React 更新后置设计

上面说到了 React 在进入更新之前有哪些操作，比如控制更新频率，防止 JS 阻塞浏览器，已经通过 Lane 处理不同优先级的更新任务，解决更新的并发场景，接下来我们看一下进入到更新之后，React 会有哪些设计方式。

**render 和 commit 阶段**

React 在进入到更新流程之后，并不是马上更新数据，更新 DOM 元素，而是通过 render 和 commit 两大阶段来处理整个流程。

在 render 阶段中，核心思想就是 diff 对比，整个 render 都围绕着 diff 展开，首先就是 React 需要通过对比 childLanes 来找到更新的组件。找到对应的组件后，就会执行组件的 render 函数，然后会得到新的 element 对象，接下来就是新 element 和老 fiber 的 diff ，通过对比对单元素节点和多元素节点来复用老 fiber ，创建新 fiber 。

在此期间，会通过对比 props 或者 state 等手段判断组件是否更新。React 开发者控制渲染的手段基本上都是在 render 阶段执行的。

在 render 阶段 React 并不会实质性的执行更新，而是只会给 fiber 打上不同的 flag 标志，证明当前 fiber 发生了什么变化。

在 render 阶段中，会通过 fiber 上面的 child ，return 和 sibling 三个指针来遍历，找到需要更新的 fiber 并且执行更新。在此其中，会采用优先深度遍历的方式，遍历 child，当没有 child 之后会遍历 sibling 兄弟节点，最后到父元素节点。这种方式的好处，就是可以方便形成真实 DOM 树结构，在 fiber 初始化流程中，创建 DOM 元素是在 render 阶段完成的。

经历了 render 阶段之后，就进入到了 commit 阶段，commit 阶段会执行更新，然后就会执行一些生命周期和更新回调函数，所以 React 开发者就可以拿到更新后的 DOM 元素。
