React 18 最主要的特性就是并发了,很多 api 都是基于并发特性实现的。
那为什么 React 要实现并发?什么是并发?又是怎么实现的呢?
这篇文章我们就一起来探究一下。
首先,我们过一遍 React 渲染的流程:
React 渲染流程
React 是通过 JSX 描述页面的,JSX 编译成 render function(也就是 React.createElement 等),执行之后产生 vdom。
![](https://i-blog.csdnimg.cn/blog_migrate/55e6a92c21271bc66f8cc8777d70fdd4.png)
vdom 是指 React Element 的对象树:
![](https://i-blog.csdnimg.cn/blog_migrate/07c2072290f8020984317ef857465b90.png)
之后这个 vdom 会转换为 fiber 结构:
![](https://i-blog.csdnimg.cn/blog_migrate/926da9a6a126b1ca0eda36704f18ca70.png)
vdom 是通过 children 关联子节点,而 fiber 通过 child、sibling、return 关联了父节点、子节点、兄弟节点。
从 vdom 转 fiber 的过程叫做 reconcile,这个过程还会创建用到的 dom 节点,并且打上增删改的标记。
这个 reconcile 的过程叫做 render 阶段。
之后 commit 阶段会根据标记来增删改 dom。
commit 阶段也分为了 3 个小阶段,before mutation、mutation、layout。
mutation 阶段会增删改 dom,before mutation 是在 dom 操作之前,layout 是在 dom 操作之后。
所以 ref 的更新是在 layout 阶段。useEffect 和 useLayoutEffect 的回调也都是在 layout 阶段执行的,只不过 useLayoutEffect 的回调是同步执行,而 useEffect 的回调是异步执行。
综上,React 整体的渲染流程就是 render(reconcile 的过程) + commit(执行增删改 dom 和 effect、生命周期函数的执行、ref 的更新等)。
当你 setState 之后,就会触发一次渲染的流程,也就是上面的 render + commit。
当然,除了 setState 之外,入口处的 ReactDOM.render 还有函数组件里的 useState 也都能触发渲染。
那么问题来了,如果同时有多个 setState 触发的渲染,怎么处理呢?
同步 vs 并发
每次 setState 都会进行上面的那个 render + commit 的渲染流程,多次那就顺序处理不就行了?
这样是能满足功能的,也就是同步模式。
但是有个问题,比如用户在 input 输入内容的时候,会通过 setState 设置到状态里,会触发重新渲染。
这时候如果还有一个列表也会根据 input 输入的值来处理显示的数据,也会 setState 修改自己的状态。
这两个 setState 会一起发生,那么同步模式下也就会按照顺序依次执行。
但如果这个渲染流程中处理的 fiber 节点比较多,渲染一次就比较慢,这时候用户输入的内容可能就不能及时的渲染出来,用户就会感觉卡,体验不好。
怎么解决这个问题呢?
能不能指定这俩 setState 的重要程度不一样,用户输入的 setState 的更新重要程度更高,如果有这种更新就把别的先暂停,执行这次更新,执行完之后再继续处理。
React 18 里确实实现了这样一套并发的机制,这里的重要程度就是优先级,也就是基于优先级的可打断的渲染流程。
React 会把 vdom 树转成 fiber 链表,因为 vdom 里只有 children,没有 parent、sibling 信息,而 fiber 节点里有,这样就算打断了也可以找到下一个节点继续处理。fiber 结构就是为实现并发而准备的。
按照 child、sibling、sibling、return、sibling、return 之类的遍历顺序,可以把整个 vdom 树变成线性的链表结构,一个循环