We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
我希望这篇文章可以帮助读者理解以下几点:
依然使用 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 链表发生在 beginWork 内。
hook 会根据组件是挂载还是更新执行不同的逻辑,对于 use(Layout)Effect 来说在挂载时会调用 mount(Layout)Effect,更新时会调用 update(Layout)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 此时也运行结束
而接下来运行的 useLayoutEffect 也与上述步骤一致,因此 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 函数打上断点来查看它的调用栈
此时我们注意到 flushPassiveEffectsImpl 这一函数,该函数在 flushPassiveEffects 内以 NormalSchedulerPriority 级别的优先级被调度,因此 flushPassiveEffectsImpl 会是一个异步任务,且优先级不高。但当前例子不需要考虑被插队的情况。
通过对 flushPassiveEffects 进行全局的查找(线索是 pendingPassiveHookEffectsMount 的赋值),最后定位调用位置为 commitBeforeMutationEffects
查询异步操作的调用很麻烦,但最后还是定位在了 commitRoot ——> commitRootImpl ——> commitBeforeMutationEffects。该函数是 BeforeMutation 阶段的入口。
在执行 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); // 环状链表循环一圈停止 } }
直接对 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; }
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); } }
The text was updated successfully, but these errors were encountered:
No branches or pull requests
React Hooks: useEffect与useLayoutEffect
TLNR
我希望这篇文章可以帮助读者理解以下几点:
阅前须知
说明
依然使用 React hooks: hooks 链表 内的示例代码。
根据之前的调试经验我们知道,每一个 hook 被调用时都会调用 mountWorkInProgressHook 去创建一个 hook 对象,并根据调用顺序构成一个链表结构,且该 hook 链表会被挂载到对应 fiber 对象的 memoizedState 属性上。
而除此之外每个不同类型的 hook 的逻辑都不相同,这里只对 useEffect 进行分析。但在此之前我们先对其结构的称呼做出约定。
effect 链表
构建 effect 链表发生在 beginWork 内。
hook 会根据组件是挂载还是更新执行不同的逻辑,对于 use(Layout)Effect 来说在挂载时会调用 mount(Layout)Effect,更新时会调用 update(Layout)Effect。
挂载时生成对应的 effect 链表
我们先对例子中的 useEffect 打入断点后会进入 mountEffect 内的 mountEffectImpl。
pushEffect 是构成 effect 链表的关键,其内生成的 effect 环状链表会被挂载到当前组件的 fiber 上的 updateQueue 上,并且 updateQueue.lastEffect 会指向最新生成的 effect 的对象。
effect 对象结构如下
执行完 mountEffectImpl 后,生成的 effect 链表会被挂载到两处,而 useEffect 此时也运行结束
而接下来运行的 useLayoutEffect 也与上述步骤一致,因此 effect 链表会变成下面这样
事实上也确实如此
至此挂载阶段生成 effect 链表的相关逻辑已经结束了。
更新时生成对应的 effect 链表
保持原有的断点,然后触发点击回调,此时会进入 updateEffect 的 updateEffectImpl
和 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 阶段内被调用
执行 effect
直接对 flushPassiveEffectsImpl 进行调试,下面的代码已经删除了多余的部分。该函数内部涉及到了对 effect 对象上的 create 与 destroy 执行的逻辑。
结论
关于 useLayoutEffect 的运行时机
useLayoutEffect 除了触发时机,其他表现与 useEffect 是一致的。
此时我们对 useLayoutEffect 的 create 函数打上断点来查看它的调用栈
此时对 commitHookEffectListMount 打断点,可发现都是同步操作。(commitLayoutEffects 是 layout 的入口)
如果对 useLayoutEffect 的 destroy 函数打上断点
此时对 commitHookEffectListUnmount 打断点,可发现都是同步操作。(commitMutationEffects 是 mutation 的入口)
结论
The text was updated successfully, but these errors were encountered: