

大家好,我是心锁,一枚23届准毕业生。
上一章,我们了解了hook是怎么赋予函数式组件状态的,也同时借此了解与调试了useState的源码,而这次,我们要动手了解useEffect是如何工作的。
上次我们在开始抛出了五个问题,目前其实解决了三个半

那么这次我们把#5过了
不过开始之前,我们还要抛出几个其他问题:
从这里开始,我们一一解答。
本次调试使用到的代码如下
const CountButton = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(v => v + 1);
};
useEffect(() => {
console.log('Hello Mount Effect');
return () => {
console.log('Hello Unmount Effect');
};
}, []);
useEffect(() => {
console.log('Hello count Effect');
}, [count]);
return (
<>
<div>Render by state</div>
<div>{count}</div>
<button onClick={handleClick}>Add Count</button>
</>
);
};!!! 请注意:需要关闭StrictMode,否则React18中的useEffect会执行两次

为了便于理解useEffect的作用原理,我整理了一些可能需要用到的前置知识点/提要
useEffect的作用流在render与commit阶段都存在,我们需要简单知道一下从render阶段进入commit的关键函数是commitRoot,换句话说commit阶段开始于commitRootLane这个词汇有关的变量统一可以先忽略,这Scheduler(调度器)有关,简而言之我还没看我也不懂Fiber.finishedWork保存的是每个Fiber的DOM操作依据,这里是在render阶段生成的,目的是避免在commit阶段再遍历一次Fiber树,保存的形式是单向链表。和useEffect没有太大关系Fiber.updateQueue,updateQueue的结构是
{ lastEffect: null, stores: null } 复制代码
其中updateQueue.lastEffect中保存的是函数式组件中调用useEffect生成的effect,effect的具体结构见下文,保存的形式和state类似是单向环状链表useEffect回调的执行时机并不在render阶段,所以render阶段主要在做的事是存储副作用
根据堆栈来看,useEffect目前处于beginWork->renderWithHooks->CountButton中,此时的运行均处于completeWork之前。

那么此时的useEffect我们可以用上一章的内容快速理解,显然mountHookTypesDev仍是一个将hook类型放入hookTypes的过程。


往下我们来到关键点mountEffect,这里会进一步调用mountEffectImpl

我们继续往下来到mountEffectImpl(我们此时并不知道PassiveXX这种变量是什么意义,但是没关系我们先跳过)
直到往下来到mountEffectImpl的主函数,我们可以知道fiberFlags===Passive|PassiveStatic,hookFlags===Passive$1

由于我们上一章有讲过mountWorkInProgressHook,已经知道这里就是hook保存使用的方法,所有本次直接来到hook.memoizedState=pushEffect(...)
pushEffect第一步声明的结构体effect存储着所有的副作用,这里的各个参数分别代表
mount时的入参为HasEffect|hookFlags,猜测和workInProgress.tag不是同一个含义,暂无需理解含义useEffect的第一个参数
undefineduseEffect的第二个参数nextfunction pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
// Circular
next: null
};
...
return effect;
}这之后,这些effect都会被挂载到currentlyRenderingFiber$1.updateQueue更新队列上(上一章我们讲到currentlyRenderingFiber$1===workInProgressFiber)
function pushEffect(tag, create, destroy, deps) {
...
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}而显然,从上述代码我们可以看到,effect和state一样,都是以单向环形链表的形式存储

往后即返回,从pushEffect的返回值看,新增的effect将挂载在hook.memoizedState上
那么截止这里,我们了解到了副作用的收集过程。
我们上次调试useState时对于update的流程有了解,需要关注updateWorkInProgressHook的话可以点击这里查看。所以这里简单说说就行。
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
...
}我们唯一需要关注的就是areHookInputsEqual,这里做了一个数组遍历比较依赖项是否更新

useEffect回调与副作用清理都在commit阶段,我们在useEffect中打上断点,然后回溯堆栈找到相关的函数


commit阶段中第一个和effect相关的即flushPassiveEffects,注意如图调用的地方,这里是通过Schedule模块进行调度的,从执行结果看,useEffect将被异步调用。注意此时的阶段

从字面意思,这里做的操作是刷新被动效果。
从代码上看,flushPassiveEffects的作用是通过**setCurrentUpdatePriority**设置优先级,然后调用**flushPassiveEffectsImpl**

关于优先级的再说,我们现在来到flushPassiveEffectsImpl
flushPassiveEffectsImpl的核心代码在这里,这里做了两件事情:
useEffect在上一次render时的返回的销毁函数useEffect在本次render时传入的函数function flushPassiveEffectsImpl() {
...
var root = rootWithPendingPassiveEffects;
{
markPassiveEffectsStarted(lanes);
}
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current);
{
markPassiveEffectsStopped();
}
...
return true;
}destory函数function commitPassiveUnmountEffects(finishedWork) {
setCurrentFiber(finishedWork);
commitPassiveUnmountOnFiber(finishedWork);
resetCurrentFiber();
}下钻到commitPassiveUnmountEffects,这里是两部分,一个是setCurrentFiber与resetCurrentFiber这里会不断把current指向当前Fiber节点然后执行effect,在执行之后则重置current

而另一部分commitPassiveUnmountOnFiber,则会根据Fiber节点不同的tag执行相应的代码

(当然不管是哪种类型目前来看都会执行recursivelyTraversePassiveUnmountEffects)
而recursivelyTraversePassiveUnmountEffects是做的是递归调用的操作,会从根节点不断向下遍历Fiber节点的子节点。不过recursivelyTraversePassiveUnmountEffects内部执行的逻辑我们现在不需要关心,对于useEffect来说,我们需要关注的是commitHookEffectListUnmount

对于函数式组件,需要执行updateQueue,区分于effectList的effect指代的一般是DOM操作,commitHookEffectListUnmount的过程实际上就是执行pushEffect时塞入updateQueue的effect的过程。
function commitHookEffectListUnmount(
flags,
finishedWork,
nearestMountedAncestor
) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
// 区分是Effect还是LayoutEffect,做开始标记
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectUnmountStarted(finishedWork);
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectUnmountStarted(finishedWork);
}
...
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
...
// 区分是Effect还是LayoutEffect,做停止标记
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectUnmountStopped();
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectUnmountStopped();
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
遍历updateQueue的过程中,react会不断取出destory并清除effect链条上的**destory**,如果destory不为空则执行

那么在此总结一下,我们知道了useEffect副作用销毁函数的时机,具体就是每次渲染,会先执行上一次useEffect生成的destory函数
create和UnMount类似,我们来到commitPassiveMountEffects,往下走同样是一个递归调用commitPassiveMountOnFiber与recursivelyTraversePassiveMountEffects的过程

这里我们只关心commitHookEffectListMount

而对于commitHookEffectListMount,基本操作都是相同的,主要的区别在于其一,会有一个把create即我们传入useEffect的第一个回调的返回值挂载到effect上,为下一次副作用预备好副作用清除函数

其二则是react中提供的一些熟悉的错误告警比如不要在useEffect中直接传入异步函数这一点
(这里又一点学到了,还有typeof destroy.then === 'function'这种判断Promise对象/async函数的方式)

那么致此,useEffect相关的调用结束
回到我们一开始抛出的问题,现在我们知道了
useEffect在render阶段做pushEffect的操作,这时会把副作用存储进updateQueue;
而在commit阶段则会通过Scheduler协调器异步执行updateQueue,先调用destory清除上次的副作用,再调用本次的create`生成新的副作用
异步执行,上述我们也看到了,useEffect通过Scheduler异步执行,根据官方说法,在React17后,useEffect异步执行,因为大部分副作用不需要延迟屏幕更新。
