React18的并发渲染机制带来的React Tearing(撕裂)问题
触发条件
- React版本18及以上
- 使用createRoot(document.getElementById('root') as HTMLElement) 方式渲染根节点、使用React18 useDeferredValue和startTransition API。
React Tearing是什么
在React18以前,React的渲染过程是不可被打断的,直到渲染流程结束之后才可以继续执行其他任务。
比如 React 现在正在渲染下面的组件树,其中子组件 Cpn4、Cpn5、Cpn6 依赖了外部的状态。React 会以 DFS (深度优先遍历)的方式去遍历整棵树,也就是说会以 Cpn1 -> Cpn2 -> Cpn4 -> Cpn5 -> Cpn3 -> Cpn6 这样的顺序来去遍历:
当渲染到 Cpn4 的时候,用户执行一个操作,去触发 Store 状态的变化,但是由于渲染并没有结束,所以会继续遍历剩余组件:
虽然用户执行改变 Store 的状态的操作,但此时需要等待渲染结束后才能真正更新 Store 状态。当整个过程结束,接下来会改变外部 Store 的状态:
整个渲染过程不会被打断,因此即使外部状态在渲染过程中被改变了,也会等到渲染完成后进行重渲染。
不过React18增加了并发更新机制,本质上就是时间切片,高优先级任务会打断低优先级任务, 在渲染过程中,一整个渲染任务被拆分成了一个个分片的渲染片段, 因此在渲染间隙时回去相应用户高优先级的任务。
React18之后的组件渲染过程
如果在渲染component4的时候,用户有高优先级任务的操作(例如点击时间),改变了外部的状态,在恢复继续渲染时就发生了状态不一致的现象,component4使用的是s1的状态,component5、component6使用的是s2的状态,这就是React Tearing(撕裂)问题,即各个组件展示的状态不一致
举个🌰
react-redux7中没有对并发更新做处理,但是升级到react-redux8就不会出现这个问题
React 提供了 useSyncExternalStore 来解决这个问题,核心原理就是将这次的并发更新变为同步更新(也就是不可中断) 。整个并发更新过程变回同步不可被中断了,自然也就不会有这个问题了。
注:非原作者,学习笔记总结。