useEffect 返回函数执行过程源码解析
作者:uccs
引言
本文对应的 react 版本是 18.2.0
在掌握 React 组件树遍历技巧中说到 react 是怎么遍历 dom
那么在遍历的过程中,如果发现当前节点有子节点被删除了,那么 react 会怎么处理呢?
下面是源码简化:这里是完整的源码
function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
const deletions = parentFiber.deletions;
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
nextEffect = childToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
childToDelete,
parentFiber
);
}
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
deletedSubtreeRoot: Fiber,
nearestMountedAncestor: Fiber | null
) {
while (nextEffect !== null) {
const fiber = nextEffect;
// 执行 passive effects 返回的函数
commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
const child = fiber.child;
if (child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot
);
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot
) {
while (nextEffect !== null) {
const fiber = nextEffect;
const sibling = fiber.sibling;
const returnFiber = fiber.return;
if (fiber === deletedSubtreeRoot) {
nextEffect = null;
return;
}
if (sibling !== null) {
sibling.return = returnFiber;
nextEffect = sibling;
return;
}
nextEffect = returnFiber;
}
}
deletions
在正式开始之前,我们要了解一个 fiber 的属性:deletions
这个属性存放的是当前节点中被删除的 fiber,这个数组是在 commit 阶段被赋值的
如果有被删除的节点,这个属性值是一个数组,如果没有被删除的节点,这个属性值是 null
const A = () => {
useEffect(() => {
return () => {
console.log("A unmount");
};
}, []);
return <div>文本A</div>;
};
const B = () => {
useEffect(() => {
return () => {
console.log("B unmount");
};
}, []);
return <div>文本B</div>;
};
如果 App 组件这样写,那么 deletions 的值是 [FiberNode, FiberNode]
const App(){
const [count, setCount] = useState(0)
return <div>
{count % 2 === 0 && <A />}
{count % 2 === 0 && <B />}
<div onClick={()=> setCount(count+1)}>+1</div>
</div>
}
如果 App 组件这样写,那么 deletions 的值是 [FiberNode]
const App(){
const [count, setCount] = useState(0)
return <div>
{count % 2 === 0 && <><A /><B /></>}
<div onClick={()=> setCount(count+1)}>+1</div>
</div>
}
对于第二种情况,react 会把 A 组件和 B 组件作为一个整体,所以 deletions 的值是 [FiberNode]
处理当前节点的 deletions
react 在遍历 fiber tree 时,会先处理当前的 fiber 的 deletions,等处理完之后再遍历下一个 fiber
现在我们已经知道 deletions 中保存的是当前 fiber 下被删除的子节点
这时 react 会遍历 deletions 数组,然后执行每个 fiber 的 passive effect 返回的函数
但是有个问题,如果 deletions 中的 fiber 有子节点,那么这些子节点也会被删除,这时 react 会怎么处理呢?
这里分两种情况来讨论:
- 删除的
fiber没有子节点:<div>{xxxx && <A />}</div> - 删除的
fiber有子节点:<div>{xxxx && <><A /><B /></>}</div>-->
删除的 fiber 没有子节点:
<div>{xxxx && <A />}</div>
这种情况比较好理解
当遍历到 div 时,因为 <A/> 节点会被卸载,所以在 div 的 deletions 保存了一个 <A/> 的 fiber
遍历 deletions 数组,执行 <A/> 的 passive effect 返回的函数
如下图所示:

删除的 fiber 有子节点:
<div>{xxxx && <><A /><B /></>}</div>
这种情况就比较复杂了
当遍历到 div 时,<></> 节点会被卸载,所以在 div 的 deletions 保存了一个 <></> 的 fiber
遍历 deletions 数组,执行 fiber 的 passive effect 返回的函数,对于 <></> 来说是不存在的 passive effect
那么这个时候就要去遍历它的 child.fiber,也就是 <A/> 和 <B/>
首先拿到第一个 fiber,也就是 <A/>,然后执行 <A/> 的 passive effect 返回的函数,这步比较好理解
child = fiber.child;
if (child !== null) {
nextEffect = child;
}
这里遍历也是深度优先,遍历一个 child,执行一个 passive effect 返回函数,然后再遍历下一个 child(这边 <A /> 已经是叶子节点了)
然后拿到第二个 fiber,也就是 <B/>,然后执行 <B/> 的 passive effect 返回的函数,这步就不太好理解了
child = fiber.child;
if (child !== null) {
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(deletedSubtreeRoot);
}
这里要注意的是:
react 在寻找有 passive effect 的 fiber 时,只遍历到有 passive effect 的 fiber, 像 div 这种没有 passive effect 就不会遍历
但是在处理 deletions,react 会遍历所有的 fiber,也就是说从当前的 fiber 开始,一直往下遍历到叶子节点,这个叶子节点是指文本节点这种,往下不会有节点了(对于 A 组件来说 文本A 是文本节点)
然后在开始往上遍历,往上遍历是调用 commitPassiveUnmountEffectsInsideOfDeletedTree_complete 函数,直到遍历到 deletionRoot,在向上遍历的过程中会检查是否有 sibling,如果有说明 sibling 还没被处理,这样就找到了 <B/>,然后执行 <B/> 的 passive effect 返回的函数
如下图所示:

向下遍历和向上遍历
在处理 deletions 时,对于每个 deletedNode,都先向下遍历,然后再向上遍历
- 向下遍历:
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(深度优先,优先处理左边的节点) - 向上遍历:
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(之后再处理右边节点)
总结
1. 遍历 deletions 数组:
react在处理deletions时,先沿着fiber tree向下遍历,如果有passive effect返回的函数,则执行- 一直遍历到没有
child的fiber,再向上遍历,处理sibling - 再向上遍历时,如果如果遇到
sibling,再向下遍历,向下遍历时遇到passive effect返回的函数,则执行 - 如此循环直到遍历到
deletedNode,结束遍历
2. 结合掌握 React 组件树遍历技巧
- 遍历寻找有
passive effect节点react从根组件向下遍历,如果没有passive effect,则不会遍历
- 遍历时,如果遇到当前节点有
deletions时,会暂停寻找passive effect节点- 进入遍历
deletions数组
- 进入遍历
react 遍历 deletions 完整逻辑如下图所示:
图中绿色部分是遍历 deletionsNode 过程,红色部分是遍历寻找 passive effect 过程

以上就是useEffect 返回函数执行过程源码解析的详细内容,更多关于useEffect 返回函数执行的资料请关注脚本之家其它相关文章!
