本文我们来看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
包含Passive
E的
副作用
为Placement
,冒泡到D,D.SubtreeFlags
包含Placement
D冒泡到C,
C.SubtreeFlags
包含Placement
C的
副作用
为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