大家好,我卡颂。
本文我们来看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
与之前版本的行为是有区别的。
考虑如下组件:
loading...}>
其中LazyCpn
是使用React.lazy
包裹的异步加载组件
。
Sibling
代码如下:
function Sibling() {
useEffect(() => {
console.log("Sibling effect");
}, []);
return Sibling
;
}
由于Suspense
会等待子孙组件中的异步请求完毕后再渲染,所以当代码运行时页面首先会渲染fallback
:
loading...
但是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直观的感受新旧版
Suspense
的差异
总结
今天我们又学到了一个React
源码小知识。
值得一提的是,针对Suspense
的这次改进,为React
带来一种新的内部组件类型 —— Offscreen Component
。
未来他可能是实现React
版keep-alive
的基础。