TLNR#
我希望这篇文章可以帮助读者理解以下几点:
- use (layout) Effect 被调用时会创建 effct 对象,且该对象会以链表的形式被挂载到 fiber 的 updateQueue 上
- useEffect 由于会被 Scheduler 以 NormalSchedulerPriority 进行调度,因此它是一个异步操作
- useLayoutEffect 的 create 是在 layout 阶段被同步处理的,其上一轮的 destroy 是在 mutation 阶段被同步处理的,因此在该 hook 内调用耗时操作会阻塞页面更新
阅前须知#
- useEffect 涉及到与 Scheduler 的交互,因此在理解其执行顺序时需要对 react 的调度行为有一定的了解。简单来说可以这样理解 (不严谨的说法会个意即可)
- 所有通过 scheduleCallback 注册进 Scheduler 的任务都是一个异步任务,且该任务类型为 Task
- react 内存在高优先级插队机制,对 useEffect 的调度可能被打断,出现与预期不符合的情况
说明#
依然使用 React hooks: hooks 链表 内的示例代码。
function UseEffectAnduseLayoutEffect() {
const [text, setText] = useState(0);
useEffect(() => {
console.log('useEffect create');
return () => {
console.log('useEffect destroy');
}
}, [text]);
useLayoutEffect(() => {
console.log('useLayoutEffect create');
return () => {
console.log('useLayoutEffect destroy');
}
}, [text]);
return (
<div onClick={ () => { setText(1) } }>{ text }</div>
)
}
根据之前的调试经验我们知道,每一个 hook 被调用时都会调用 mountWorkInProgressHook 去创建一个 hook 对象,并根据调用顺序构成一个链表结构,且该 hook 链表会被挂载到对应 fiber 对象的 memoizedState 属性上。
而除此之外每个不同类型的 hook 的逻辑都不相同,这里只对 useEffect 进行分析。但在此之前我们先对其结构的称呼做出约定。
use(Layout)Effect(() => { // 约定其第一个参数为 create
return () => { // 约定 create 函数的返回值为 destroy
}
}, []); // 约定其第二个参数为 依赖
effect 链表#
构建 effect 链表发生在 beginWork 内。
hook 会根据组件是挂载还是更新执行不同的逻辑,对于 use (Layout) Effect 来说在挂载时会调用 mount (Layout) Effect,更新时会调用 update (Layout) Effect。
挂载时生成对应的 effect 链表#
我们先对例子中的 useEffect 打入断点后会进入 mountEffect 内的 mountEffectImpl。
mountEffectImpl(
UpdateEffect | PassiveEffect, // react 中关于权限存在大量的位操作,此处的**或**操作常有赋予权限的意味
HookPassive,
create,
deps,
);
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
/**
* hook 链表内有分析过因此跳过
*/
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
/**
* A |= B => A = A | B
*/
currentlyRenderingFiber.flags |= fiberFlags;
/**
* 此处的逻辑是一处重点,涉及到 effect 对象的创建与链表的连接
* 还需要注意到的 useEffect 对应的 hook 对象上的 memoizedState 会挂载上 effect 链表
* useLayoutEffect 的逻辑和 useEffect 大致相同,只是会对 effect 对象打上不同的标签而已
*/
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
pushEffect 是构成 effect 链表的关键,其内生成的 effect 环状链表会被挂载到当前组件的 fiber 上的 updateQueue 上,并且 updateQueue.lastEffect 会指向最新生成的 effect 的对象。
effect 对象结构如下
const effect: Effect = {
tag,
create, // use(Layout)Effect 的 create
destroy, // use(Layout)Effect 的 destroy
deps, // use(Layout)Effect 的 依赖
// Circular
next: (null: any), // 指向下一个effect
};
执行完 mountEffectImpl 后,生成的 effect 链表会被挂载到两处,而 useEffect 此时也运行结束
- use (Layout) Effect 对应 hook 元素的 memoizedState
- fiber.updateQueue
而接下来运行的 useLayoutEffect 也与上述步骤一致,因此 effect 链表会变成下面这样
事实上也确实如此
至此挂载阶段生成 effect 链表的相关逻辑已经结束了。
更新时生成对应的 effect 链表#
保持原有的断点,然后触发点击回调,此时会进入 updateEffect 的 updateEffectImpl
updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
)
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook(); // 更新情况下,会对 currentHook 进行赋值
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState; // 获取 currentHook 对应的 effect 对象
destroy = prevEffect.destroy; // 将上一次的 destroy 函数赋值给 wip 并在这一次调用
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) { // 一一比较新旧的dep,不过是浅对比
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // HookHasEffect 标记更新
create,
destroy,
nextDeps,
);
}
和 mount 不同,update 的 pushEffect 会带上上一次的 destroy 函数。
关于 useEffect 内 create 与 destroy 运行顺序#
此时我们对 useEffect 的 create 函数打上断点来查看它的调用栈
此时我们注意到 flushPassiveEffectsImpl 这一函数,该函数在 flushPassiveEffects 内以 NormalSchedulerPriority 级别的优先级被调度,因此 flushPassiveEffectsImpl 会是一个异步任务,且优先级不高。但当前例子不需要考虑被插队的情况。
通过对 flushPassiveEffects 进行全局的查找(线索是 pendingPassiveHookEffectsMount 的赋值),最后定位调用位置为 commitBeforeMutationEffects
查询异步操作的调用很麻烦,但最后还是定位在了 commitRoot ——> commitRootImpl ——> commitBeforeMutationEffects。该函数是 BeforeMutation 阶段的入口。
收集 effect#
在执行 flushPassiveEffectsImpl 清空 effect 前,首先需要对 fiber 上的 effect 链表进行收集,这一操作发生在 schedulePassiveEffects 内。由于 schedulePassiveEffects 是在 commitLayoutEffects ——> commitLifeCycles 内被调用,因此可以视为在 layout 阶段内被调用
/**
* 此处的 finishedWork 为 UseEffectAnduseLayoutEffect 的 fiber 对象
*/
function schedulePassiveEffects(finishedWork: Fiber) {
/**
* fiber 的 updateQueue上挂载了对应的 effect 链表
*/
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
// lastEffect这一指针是指向最后一个 effect 对象上的,因此通过 next 可以直接获取到第一个元素
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
if (
(tag & HookPassive) !== NoHookEffect && // 过滤了非 use(Layout)Effect 创建的 effect 对象
(tag & HookHasEffect) !== NoHookEffect // 过滤了依赖未发生变动的 effect 对象
) {
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); // 给pendingPassiveHookEffectsUnmount赋值
enqueuePendingPassiveHookEffectMount(finishedWork, effect);// 给pendingPassiveHookEffectsMount赋值
// 目前执行完 pendingPassiveHookEffectsUnmount 与 pendingPassiveHookEffectsMount数据一致
}
effect = next;
} while (effect !== firstEffect); // 环状链表循环一圈停止
}
}
执行 effect#
直接对 flushPassiveEffectsImpl 进行调试,下面的代码已经删除了多余的部分。该函数内部涉及到了对 effect 对象上的 create 与 destroy 执行的逻辑。
function flushPassiveEffectsImpl() {
...
/**
* 执行 effect 的 destroy 函数
* 在挂载时,由于在执行 pushEffect 时第三个参数为 undefined,effect 对象上的 destroy 属性为空。因此 destroy 不会被执行
* 但是在更新时,会传入由 create 返回的 destroy 函数,因此会执行。下面会提到。
*/
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
try {
startPassiveEffectTimer();
destroy();
} finally {
recordPassiveEffectDuration(fiber);
}
} else {
destroy();
}
}
}
/**
* 执行 effect 的 create 函数
*/
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
const create = effect.create;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
try {
startPassiveEffectTimer();
/**
* create 的返回值是 destroy,如果这个 effect 对象因为 update 再次被收集后,
* 会在下一次flushPassiveEffectsImpl时被指向
*/
effect.destroy = create();
} finally {
recordPassiveEffectDuration(fiber);
}
} else {
effect.destroy = create();
}
}
...
return true;
}
结论#
- useEffect 是异步操作,清空副作用的操作(flushPassiveEffectsImpl)会被注册为一个 Task
- layout 阶段中 effect 会被收集进执行数组,但是调度发生在 before Mutation 调度
- 清空副作用的操作(flushPassiveEffectsImpl)的调度优先级不高,所以有可能在调度时有可能被更高优先级的任务插队
- 挂载时,只会执行 create 的函数;更新时,会执行 currentHook 的 destroy,然后执行 newHook 的 create
关于 useLayoutEffect 的运行时机#
useLayoutEffect 除了触发时机,其他表现与 useEffect 是一致的。
此时我们对 useLayoutEffect 的 create 函数打上断点来查看它的调用栈
此时对 commitHookEffectListMount 打断点,可发现都是同步操作。(commitLayoutEffects 是 layout 的入口)
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
// 传入的 tag 是 HookLayout | HookHasEffect,因此可过滤出 useLayoutEffect 的 effect
if ((effect.tag & tag) === tag) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect); // 循环一圈
}
}
如果对 useLayoutEffect 的 destroy 函数打上断点
此时对 commitHookEffectListUnmount 打断点,可发现都是同步操作。(commitMutationEffects 是 mutation 的入口)
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
结论#
- useLayoutEffect 是同步调用的,其 destroy 在 mutation 阶段调用,其 create 在 layout 阶段调用