本文我们来看React内部Effects List机制重构的前因后果。
阅读完本文,你可以掌握React18对比之前版本,Suspense特性的差异及原因。
什么是副作用
简易的React工作原理可以概括为:
触发
更新render阶段:计算
更新会造成的副作用commit阶段:执行
副作用
副作用包含很多类型,比如:
Placement指DOM节点的插入与移动Passive指useEffect回调执行ChildDeletion指移除子DOM节点等等
更新造成DOM变化主要就是Placement、ChildDeletion在起作用。
那么render阶段如何保存副作用,commit阶段又是如何使用副作用的呢?
Effects List
在重构前,render阶段,带有副作用的节点会连接形成链表,这条链表被称为Effects List。
比如下图,B、C、E存在副作用,连接形成Effects List:

commit阶段不需要从A向下遍历整棵树,只需要遍历Effects List就能找到所有有副作用的节点并执行对应操作。
SubtreeFlags
在重构之后,会将子节点的副作用冒泡到父节点的SubtreeFlags属性。
比如B、C、E包含的副作用如下图:

冒泡流程如下:
B的
副作用为Passive,冒泡到A,A.SubtreeFlags包含PassiveE的
副作用为Placement,冒泡到D,D.SubtreeFlags包含PlacementD冒泡到C,
C.SubtreeFlags包含PlacementC的
副作用为Update,C.SubtreeFlags包含Placement,C冒泡到A最终
A.SubtreeFlags包含Passive、Placement、Update
这就代表A的子树中包含这三种副作用。
在commit阶段,再根据SubtreeFlags一层层查找有副作用的节点并执行对应操作。
可见,SubtreeFlags需要遍历树,而Effects List只需要遍历链表,效率更高。那么React为什么要重构呢?
Suspense
答案是:SubtreeFlags遍历子树的操作虽然比Effects List需要遍历更多节点,但是React18中一种新特性恰恰需要「遍历子树」。
这个特性就是Suspense。
Suspense是v16就提供的功能,但v18之后,当开启并发功能,Suspense与之前版本的行为是有区别的。
考虑如下组件:
<Suspense fallback={<h3>loading...</h3>}>
<LazyCpn />
<Sibling />
</Suspense>其中LazyCpn是使用React.lazy包裹的异步加载组件。
Sibling代码如下:
function Sibling() {
useEffect(() => {
console.log("Sibling effect");
}, []);
return <h1>Sibling</h1>;
}由于Suspense会等待子孙组件中的异步请求完毕后再渲染,所以当代码运行时页面首先会渲染fallback:
<h3>loading...</h3>但是Sibling并不是异步的!这里就体现了新旧版本React的差异。
新旧版React的差异
再回顾下开篇介绍的简易React工作原理:
触发
更新render阶段:协调器计算
更新会造成的副作用commit阶段:渲染器执行
副作用
在开启并发之前,React保证一次render阶段对应一次commit阶段。
所以在上例中,虽然由于LazyCpn在请求导致Suspense渲染fallback,但是并不会阻止Sibling渲染,也不会阻止Sibling中useEffect的执行。
控制台还是会打印「Sibling effect」。
同时,为了在视觉上显得Sibling没有渲染,Sibling渲染的DOM节点会被设置display: none:

但这其实挺hack的。毕竟根据Suspense的理念,如果子孙组件有异步加载的内容,那应该只渲染fallback(而不是同时渲染display: none的内容)
所以在新版中,针对Suspense内「不显示的子树」做了单独的处理,既不会渲染display: none的内容,也不会执行useEffect回调:

要实现这部分处理的基础,就是改变commit阶段遍历的方式,也就回到开篇提到的Effects List重构为subtreeFlags。
你可以从这个在线Demo[1]直观的感受新旧版
Suspense的差异
总结
今天我们又学到了一个React源码小知识。
值得一提的是,针对Suspense的这次改进,为React带来一种新的内部组件类型 —— Offscreen Component。
未来他可能是实现React版keep-alive的基础。
参考资料
[1]
在线Demo: https://codesandbox.io/s/frosty-currying-35olk?file=/src/App.js
本文介绍了React18中EffectsList机制的重构,以及这一变化与Suspense特性之间的关系。在重构后,React使用SubtreeFlags来存储和处理副作用,提高了效率。然而,Suspense的新行为需要遍历子树,这促使了这种重构。在新版本中,Suspense在并发模式下避免了非异步组件的渲染和副作用执行,提供更纯净的渲染效果。

1万+

被折叠的 条评论
为什么被折叠?



