大家好,我是心锁,一枚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阶段开始于commitRoot
Lane
这个词汇有关的变量统一可以先忽略,这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
的第一个参数undefined
useEffect
的第二个参数next
function 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异步执行,因为大部分副作用不需要延迟屏幕更新。