阅读图解React笔记10
2022 react本文仅仅是阅读 图解 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.destroy
function 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(),结束整个闭环。