# 阅读图解React笔记10
Table of Contents
本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。
副作用 Hook
创建 Hook
在 fiber 初次构造阶段,useEffect 对应源码 mountEffect,useLayoutEffect 对应的源码 mountLayoutEffect
function mountEffect(create: () => (() => void) | void, deps: Array<mixed> | void | null): void { return mountEffectImpl(PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);}function mountLayoutEffect(create: () => (() => void) | void, deps: Array<mixed> | void | null) { return mountEffectImpl(UpdateEffect, HookLayout, create, deps);}可见 mountEffect 和 mountLayoutEffect 内部都是通过调用 mountEffectImpl。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);}mountEffectImpl 逻辑:
- 创建 hook。
- 设置 workInProgress 的副作用标记:flags |= fiberFlags。
- 创建 effect,挂载到 hook.memoizedState 上,即 hook.memoizedState = effect
PS:状态 Hook 的 hook.memoizedState = state。
创建 Effect
function pushEffect(tag, create, destroy, deps) { // 创建 effect 对象 const effect: Effect = { tag, create, destroy, deps, next: (null: any), }; // 将 effect 对象添加到环形链表末尾 let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); componentUpdateQueue.lastEffect = effect.next = effect; } else { const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect;}effect 的数据结构:
export type Effect = { tag: HookFlags, create: () => (() => void) | void, destroy: (() => void) | void, deps: Array<mixed> | null, next: Effect,};-
effect.tag: HookFlags 类型定义位于 react-reconciler/src/ReactHookEffectTags.js 中,是二进制属性,代表 effect 类型:
export type HookFlags = number;export const NoFlags = /* */ 0b000;export const HasEffect = /* */ 0b001;export const Layout = /* */ 0b010;export const Passive = /* */ 0b100; -
effect.create: 通过 useEffect() 所传入的函数
-
effect.deps:依赖项,如果依赖变动,会创建新的 effect
现在 workInProgress.flags 被打上标记,最后会在 fiber 树渲染阶段的 commitRoot 函数中处理。
区别
从上面可知,useEffect 与 useLayoutEffect 只是内部 flags,tag 状态的区别
- fiber.flags 区别:
- useEffect 的
fiber.flags = UpdateEffect | PassiveEffect - useLayoutEffect 的
fiber.flags= UpdateEffect
- useEffect 的
- fiber.tag 的区别:
- useEffect 的
fiber.tag = HookHasEffect | HookPassive - useLayoutEffect 的
fiber.tag = HookHasEffect | HookLayout
- useEffect 的
处理 Effect 回调
完成 fiber 树构造后,逻辑会进入渲染阶段,在 commitRootImp 函数中,整个渲染过程被 3 个函数分步实现:
- commitBeforeMutationEffects
- commitMutationEffects
- recursivelyCommitLayoutEffects
这 3 个函数会处理 fiber.flags,也会根据情况处理 fiber.updateQueue.lastEffect。
commitMutationEffects
function commitBeforeMutationEffects(firstEffect: Fiber) { let fiber = firstEffect; while (fiber !== null) { if (fiber.deletions !== null) { commitBeforeMutationEffectsDeletions(fiber.deletions); }
if (fiber.child !== null) { const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask; if (primarySubtreeFlags !== NoFlags) { commitBeforeMutationEffects(fiber.child); } }
try { commitBeforeMutationEffectsImpl(fiber); } catch (error) { captureCommitPhaseError(fiber, fiber.return, error); } fiber = fiber.sibling; }}
function commitBeforeMutationEffectsImpl(fiber: Fiber) { const flags = fiber.flags;
// 省略其他代码,只保留 Hook 相关
if ((flags & Passive) !== NoFlags) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null; }); } }}第一阶段:DOM 变更之前,处理副作用队列中带有 Passive 标记的 fiber 节点。
注意:由于 flushPassiveEffects 被包裹在 scheduleCallback 回调中,由调度中心来处理的,且参数是 NormalSchedulerPriority,故这是一个异步回调。
由于 scheduleCallback(NormalSchedulerPriority, callback) 是异步的,flushPassiveEffects 并不会立即执行。
commitMutationEffects
function commitMutationEffects( firstChild: Fiber, root: FiberRoot, renderPriorityLevel: ReactPriorityLevel) { let fiber = firstChild; while (fiber !== null) { const deletions = fiber.deletions; if (deletions !== null) { commitMutationEffectsDeletions(deletions, fiber, root, renderPriorityLevel); }
if (fiber.child !== null) { const mutationFlags = fiber.subtreeFlags & mutationMask; if (mutationFlags !== NoFlags) { commitMutationEffects(fiber.child, root, renderPriorityLevel); } }
try { commitMutationEffectsImpl(fiber, root, renderPriorityLevel); } catch (error) { captureCommitPhaseError(fiber, fiber.return, error); } fiber = fiber.sibling; }}
function commitMutationEffectsImpl(fiber: Fiber, root: FiberRoot, renderPriorityLevel) { const flags = fiber.flags;
// 省略其他代码,只保留 Hook 相关
const primaryFlags = flags & (Placement | Update | Hydrating);
switch (primaryFlags) { case Update: { const current = fiber.alternate; commitWork(current, fiber); break; } }}
function commitWork(current: Fiber | null, finishedWork: Fiber): void { // 省略其他代码,只保留 Hook 相关
switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: case Block: { commitWorkEffectListUnmount(HookLayout | HookHasEffect, finishedWork, finishedWork.return); return; } }}
// 依次执行 effect.destroyfunction commitHookEffectListUnmount( flags: HookFlags, finishedWork: Fiber, nearestMountedAncestor: Fiber | null) { 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 & flags) === flags) { const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); } } effect = effect.next; } while (effect !== firstEffect); }}第二阶段:DOM 变更,界面得到更新。
调用关系:commitMutationEffects -> commitWork -> commitHookEffectListUnmount。
- 注意在调用 commitMutationEffects(HookLayout | HookHasEffect, finishedWork) 时,参数是 HookLayout | HookHasEffect,所以只处理由 useLayoutEffect() 创建的 effect。
- 根据上文的分析 HookLayout | HookHasEffect 是通过 useLayoutEffect 创建的 effect。所以 commitMutationEffects 函数只能处理由 useLayoutEffect() 创建的 effect。
- 同步调用 effect.destroy()。
recursivelyCommitLayoutEffects
function recursivelyCommitLayoutEffects(finishedWork: Fiber, finishedRoot: FiberRoot) { const { flags, tag } = finishedWork;
switch (tag) { case Profiler: { // 省略其他代码 }
default: { // 省略其他代码 const primaryFlags = flags & (Update | Callback); if (primaryFlags !== NoFlags) { switch (tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) { schedulePassiveEffectCallback(); } break; } } } } }}
function commitHookEffectListMount(flags: HookFlags, 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 & flags) === flags) { const create = effect.create; effect.destroy = create(); } effect = effect.next; } while (effect !== firstEffect); }}更新 Hook
在更新过程中 useEffect 对应的源码 UpdateEffect,useLayoutEffect 对应的源码 updateLayoutEffect。他们内部都是调用 UpdateEffectImpl,与初次创建时一样,只是参数不同。
更新 Effect
function UpdateEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook(); 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)) { pushEffect(hookFlags, create, destroy, nextDeps); return; } } }
// 如果依赖变动,更改 fiber.flag,新建 effect currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);}UpdateEffectImpl 与 mountEffectImpl 逻辑有所不同,如果 useEffect/useLayoutEffect 的依赖不变,新建的 effect 对象不带 HasEffect 标记。
注意:无论依赖是否变化,都复用之前的 effect.destroy。等待 commitRoot 阶段的调用。
处理 Effect 回调
新的 Hook 以及新的 Effect 创建完成之后,余下逻辑与初次渲染完全一致。处理 Effect 回调时也会根据 effect.tag 进行判断:只有effect.tag 包含 HookHasEffect 时才会调用 effect.destroy 和 effect.create()
组件销毁
当 function 组件被销毁时,fiber 节点必然会被打上 Deletion 标记,即 fiber.flags |= Deletion,带有 Deletion 标记的 fiber 在 commitMutationEffect 被处理
在 commitDeletion 函数之后,继续调用 unmountHostComponents -> commitUnmount,在 commitUnmount 中,执行 effect.destroy(),结束整个闭环。