【学习目标】 看懂下面代码的执行顺序:
js
function Son(){
React.useEffect(() =>{
console.log( --------Son useEffect------- )
})
React.useLayoutEffect(() =>{
console.log( --------Son useLayoutEffect------- )
})
React.useInsertionEffect(() =>{
console.log( --------Son useInsertionEffect------- )
})
return <div>子组件</div>
}
function Father(){
React.useEffect(() =>{
console.log( --------Father useEffect------- )
})
React.useLayoutEffect(() =>{
console.log( --------Father useLayoutEffect------- )
})
React.useInsertionEffect(() =>{
console.log( --------Father useInsertionEffect------- )
})
return <div>
<div>父组件</div>
<Son/>
</div>
}
猜到打印顺序了吗?
js
--------Son useInsertionEffect-------
--------Father useInsertionEffect-------
--------Son useLayoutEffect-------
--------Father useLayoutEffect-------
--------Son useEffect-------
--------Father useEffect-------
这段打印结果反应了两个问题
第一, useInsertionEffect,useLayoutEffect,useEffect 的执行顺序都是先子后父
第二,为什么 useInsertionEffect,useLayoutEffect,useEffect 的顺序是这样的?
那我们 业务代码 联动 commit源码就懂了
js
function Index(){
const [color, setColor ]= React.useState( #000 )
React.useEffect(() =>{
console.log( --------useEffect------- )
})
React.useLayoutEffect(() =>{
console.log( --------useLayoutEffect------- )
})
React.useInsertionEffect(() =>{
console.log( --------useInsertionEffect------- )
})
return <div>
<div id ="text" style ={{ color }}> hello,react </div>
<button onClick ={() => setColor( red )} >点击改变颜色</button>
</div>
}
js
function commitRootImpl(){
if ((finishedWork.subtreeFlags & PassiveMask)! = = NoFlags || (finishedWork.flags & PassiveMask)! = = NoFlags) {
/* 通过异步的方式处理 useEffect */
scheduleCallback $1(NormalPriority, function () {
flushPassiveEffects();
return null;
});
}
/* BeforeMutation 阶段执行 */
const text = document.getElementById( text )
console.log( -----BeforeMutation 执行------- )
commitBeforeMutationEffects(root, finishedWork);
console.log( -----BeforeMutation 执行完毕------ )
/* Mutation 阶段执行 */
console.log( -----Mutation 执行----- )
if(text) console.log( 颜色获取:,window.getComputedStyle(text).color)
commitMutationEffects(root, finishedWork, lanes);
console.log( -----Mutation 执行完毕----- )
if(text) console.log( 颜色获取:,window.getComputedStyle(text).color)
/* Layout 阶段执行 */
console.log( -----Layout 执行----- )
commitLayoutEffects(finishedWork, root, lanes);
console.log( -----Layout 执行完毕----- )
}
打印顺序如下
js
-----BeforeMutation 执行-------
-----BeforeMutation 执行完毕------
-----Mutation 执行-----
颜色获取:rgb(0,0,0)
--------useInsertionEffect-------
-----Mutation 执行完毕-----
颜色获取:rgb(255,0,0)
-----Layout 执行-----
--------useLayoutEffect-------
-----Layout 执行完毕-----
--------useEffect-------
从源码和打印顺序我们看到:
-
commit阶段 分为三个阶段 :BeforeMutation,Mutation,Layout
-
useInsertionEffect 是在 mutation 阶段执行的,虽然 mutation 是更新 DOM,但是 useInsertionEffect 是 在更新 DOM 之前。
-
useLayoutEffect 是在 layout 阶段执行,此时 DOM 已经更新了。
-
useEffect 是在浏览器绘制之后异步执行的
-
真实 DOM 的改变确实是在 mutation 阶段执行的, 在 mutation 前后的两次打印,可以看出打印颜色的变化
更新标志
还记得上一篇 # 《深入浅出react》总结之 10.2 渲染阶段流程探秘-completeWork 我们提到的 subtreeFlags, subtreeFlags 用来标记子树中的副作用操作
这些 subtreeFlags 可不是随便打的,要标记是删除还是更新或者插入等操作,react会给不同的副作用subtreeFlags分门别类,怎么分类呢?刚才我们看到各种副作用操作 分别在 commit 不同阶段触发,所以 就有下面的分类:
js
/* Before Mutation 阶段标志*
/var BeforeMutationMask = Update | Snapshot
/* Mutation 阶段标志*
/var MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Visibility;
/* Layout 阶段标志 */
var LayoutMask = Update |Callback |Ref |Visibility;
/* useEffect 阶段标志 */
var PassiveMask = Passive | ChildDeletion;
subtreeFlags 打上什么标记,就在什么阶段触发
beforeMutation 阶段
上代码
js
function commitBeforeMutationEffects_begin() {
while (nextEffect ! = = null) {
var fiber = nextEffect;
var child = fiber.child;
if ((fiber.subtreeFlags & BeforeMutationMask) ! = = NoFlags && child ! = = null) {
/* 这里如果子代 fiber 树有 Before Mutation 的标志,那么把 nextEffect 赋值给子代 fiber */
nextEffect = child;
} else {
/* 找到最底层有 Before Mutation 标志的 fiber,执行 complete */
commitBeforeMutationEffects_complete();
}
}
}
commitBeforeMutationEffects_begin 流程主要做了两件事,
- 如果子代 fiber 树有 Before Mutation 的标志,那么把nextEffect 赋值给子代 fiber。这里可以理解 begin 会向下递归,找到最底部并且有此标志的 fiber。
- 找到最底层有 Before Mutation 标志的 fiber,执行 complete。
js
function commitBeforeMutationEffects_complete(){
while (nextEffect ! = = null) {
var fiber = nextEffect;
try{
/* 真正的处理 Before Mutation 需要做的事情。*/
commitBeforeMutationEffectsOnFiber(fiber);
}
/* 优先处理兄弟节点上的 Before Mutation */
var sibling = fiber.sibling;
if (sibling ! = = null) {
nextEffect = sibling;
return;
}
/* 如果没有兄弟节点,那么返回父级节点,继续进行如上流程。*/
nextEffect = fiber.return;
}
}
complete 的流程是向上归并的流程,
- 首先会执行 commitBeforeMutationEffectsOnFiber 真正处理Before Mutation 需要做的事情。
- 在向上归并的过程中,会先处理兄弟节点上的 Before Mutation,如果没有兄弟节点,那么返回父 级节点,继续进行如上流程
最后再来看看 commitBeforeMutationEffectsOnFiber
js
function commitBeforeMutationEffectsOnFiber(){
if ((flags & Snapshot) ! = = NoFlags) { /* 如果有 Snapshot 标志 */
switch (finishedWork.tag) {
case ClassComponent:
var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType = = =
finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
}
commitBeforeMutationEffectsOnFiber就是 执行 Before Mutation 阶段标志的副作用函数: Snapshot
看完这个整遍历体流程,有没有感觉很像 performUnitOfWork过程(beginWork 递归到 最底层fiber,completeWork 一路向上) 我可以说真的太像了,所以都叫 begin 和 complete, 只不过 commitBeforeMutationEffects_begin不会做其他工作,只是向下遍历
mutation 阶段
到了 mutation,其实Mutation begin 做的事情,除了和 BeforeMutation 一样,找到最底层有 Mutation 标志的 fiber,执行 complete 外,
还有一件事情就是通过调用 commitDeletion 来执行删除元素操作, 并执行 componentWillUnmount,useLayoutEffect 的 destroy函数(从 父 到 子)
而 Mutation complete阶段呢?
会根据不同的 subTreeFlags 标记,处理下面的情况:
var MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Visibility;
ContentReset: 重置节点内容
Ref:重置alternate属性上的ref
Placement:插入新节点
并且会执行 :useInsertionEffect
在 effect 的执行特点上,所有的 effect hooks 会先执行上一次的 destroy 函数,然后调用本次的,create 函数,比如在 effect 里面绑定事件监听器,如果绑定新的监听器,需要先解绑老的监听器。
layout 阶段
这里不再赘述了
我们直接上commit 整体流程图吧

回到开头
所以我们再看一下开头就会明白,为什么是从子到父 触发这些 effect?
因为 这些 effect 都是在 各个commit complete阶段 向上遍历时触发的
从 设计理念 上也说的通:
1. DOM 操作安全性
- 父组件操作可能依赖子组件的 DOM 结构(如计算位置/尺寸)
- 子组件先完成 DOM 更新,父组件才能安全访问子组件的 DOM
js
// 父组件在 useLayoutEffect 中访问子元素
useLayoutEffect(() => {
// 子元素必须已插入 DOM 才能正确获取
const childHeight = document.getElementById("child").offsetHeight;
}, []);
2. Effect 依赖链保证
- 子组件的副作用可能影响父组件状态
- 子组件优先执行可确保父组件获得最新数据
js
// 子组件
useLayoutEffect(() => {
updateParentData(data); // 先更新父组件依赖的数据
}, [data]);
// 父组件
useLayoutEffect(() => {
console.log("父组件使用子组件更新的数据");
}, [parentData]);