Skip to content

React18 并发原理 ​

1. 什么是并发 ​

并发 ---- 吃饭吃到一半,电话来了,先去接电话,然后回来继续吃饭。 并行 ---- 吃饭吃到一半,电话来了,一边吃饭一边打电话。 并发是 ** 交替处理多个任务 **。

2. React 为什么需要并发 ​

js 是单线程语言,同一时间只能执行一件事情。如果有一个耗时任务占据了线程,那么后续的执行内容都会被阻塞。

3. 浏览器的一帧里做了什么? ​

  1. 用户事件:最先执行,比如 click 等事件。
  2. js 代码: 宏任务和微任务。React 时间切片也会在这里执行。浏览器觉得宏任务执行时间太久,会下一个宏任务分配到下一帧,避免掉帧。
  3. 在渲染前执行 scroll/resize 等事件回调。
  4. 在渲染前执行 requestAnimationFrame 回调。
  5. html、css 的计算布局绘制
  6. requestIdleCallback 执行回调:如果前面的那些任务执行完成了,一帧还剩余时间,那么会调用该函数。 由于 requestIdleCallback 的兼容性比较差,React 决定自己根据 MessageChannel,自己来实现。

4. 时间切片 ​

React 让每一帧只花 5ms 来更新。 解决思路如下:

  • 更新开始,记录开始时间 startTime。
  • js 代码执行时,记录距离开始时间 startTime 是否超过了 5ms。
  • 如果超过了 5ms,那么这个时候就不应该再以同步的形式来执行代码了,否则依然会阻塞后续的代码执行。 所以这个时候我们需要把后续的更新改为一个宏任务,这样浏览器就会分配给他执行的时机。如果有用户事件进来,那么会执行用户事件,等用户事件执行完成后,再继续执行宏任务中的更新。

5. Fiber 架构 ​

虚拟 DOM 一旦暂停对比过程,下次更新时比较麻烦。所以 React 引入了 Fiber 架构。 Fiber 和 虚拟 DOM 最大区别是多加了几个属性:

  • return 表示父节点 fiber。
  • child 表示子节点的第一个 fiber。
  • sibling 表示下一个兄弟节点的 fiber。

6. 小结 ​

  • 遍历更新每一个 fiber 节点,判断累计时间是否超过 5ms
  • 如果超过 5ms,则会开启一个宏任务,浏览器自动分配执行时机,从而不阻塞用户事件。
  • 如果更新过程触发了 click 事件,那么会在 5ms 与下一个 5ms 的间隙去执行 click 事件的回调。

7. Scheduler 调度 ​

React 通过 Scheduler 库来处理时间分片。 遍历 Fiber 节点,创建 Fiber 树,并且标记哪些 Fiber 被更新。 通过 MessageChannel 来开启宏任务,进入到下一次更新。

8. 更新优先级 ​

React 更新分为紧急更新(Urgent updates)和过渡更新(Transition updates)。 React 为通过 lane 来分配优先级。 举例:假设有一个更新对同一个属性 count 进行:

js
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
  A按钮
</button>
<button onClick={() => startTransition(() => { setCount(count + 1) })}>
  B按钮
</button>

先点击 B,又点击 A,如果 B 还没更新完,会通过 lan 进行对比,此时会中断 B 更新,开始 A 更新。直到 A 更新完,再重新开始 B 更新。 image

9. React18 并发渲染 ​

  1. 为什么需要并发更新? 期望一些不重要的更新不会影响用户操作。

  2. 并发模式是怎样的? 多个更新并存的情况,优先执行紧急更新,其次再执行不紧急更新。

  3. 并发模式如何实现? 通过分配 lane,来区分紧急程度。 通过 Fiber 结构将不紧急的更新拆分为多段,通过宏任务合理的分配到不同的帧当中。 高优先的任务会打断低优先级的任务,等高优先级的任务完成之后,再开始低优先级的任务。

10. 新特性 ​

Suspense ​

v16/v17 版本 Suspense 主要用来 code spliting。 v18 用于将 读取数据 和 指定加载状态分离。

fallback 是指定加载状态或者骨架,List 里面主要用于读取数据。 实现的原理是 unstable_createResource 会抛出一个错误 Promise 被 Suspense 捕获,Promise 执行成功之后重新渲染 组件。

jsx
const wrappedData = unstable_createResource((pageId) => fetchData(pageId));

function List({ pageId }) {
  const data = wrappedData.read(pageId);

  return data[pageId].map((item) => <li>{item}</li>);
}

// 在需要使用 List 组件的地方包裹一层  Suspense 即可自动控制加载抓昂太
<Suspense fallback={<div>Loading...</div>}>
  <List />
</Suspense>;

useTransition/useDeferredValue ​

useDeferredValue 将紧急的事变成不紧急的。 而 useTransition 未来可能会用于 routers (page navigations) or data fetching libraries (refetching data)。 和防抖截流的区别是: 防抖和截流必须设置一个阀值,如果设置过长,如果列表已经渲染更新很快,则有点浪费。 如果设置过短,无法解决列表渲染过长的问题。

useSyncExternalStore ​

假设一个低优先的任务又 40 个小更新,它们都获取一个全局状态的值 globalVariable = 1,当更新到第 20 个的时候,突然有个事件触发,让 globalVariable = 2,这个时候后续的 20 个更新和前面的 20 个更新就会不一致。我们称之为 tearing 撕裂。

当并发更新发生变化的时候,useSyncExternalStore 会让它进行强制渲染。