跳到主要内容

认识

2023年06月10日
柏拉文
越努力,越幸运

一、认识


useEffect 初始渲染或者状态更新之后, 在 commit 阶段完成以后异步执行,防止同步执行时阻塞浏览器渲染。对于不同的 effect 钩子, 父子组件的执行顺序是: 先子后父

二、细节


2.1 effect

const effect: Effect = {
tag,
create,
inst,
deps,
next: (null: any), // 环形链表
};

effect 对象中有 create 回调函数、deps 依赖项、存储 effect 专属的 fiber flag、以及 next 组成的环形链表

2.1 fiber.updateQueue

fiber.updateQueue 存储 useEffecteffect 副作用链表

const fiber = currentlyRenderingFiber;
const updateQueue = fiber.updateQueue;
if (updateQueue == null) {
const updateQueue = createFCUpdateQueue();
fiber.updateQueue = updateQueue;
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const lastEffect = updateQueue.lastEffect;
if (lastEffect == null) {
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
}
}

2.2 hook.memoizesState

hook.memoizesState 存储 useEffecteffect 副作用链表

hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
undefined,
nextDeps
);

2.3 fiber.memoizedState

fiber.memoizedState 存储 Hooks 链表

if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = hook;
workInProgressHook = hook;
}

三、执行函数组件


React.jsbeginWork 阶段, 遇到 FunctionComponent 类型的 Fiber, 会执行 updateFunctionComponentupdateFunctionComponent 内部调用 renderWithHooks。主要工作如下:

  1. 重置状态: 函数组件执行前, 重置当前正在处理的 hook、当前正在使用的 hook

  2. 记录当前组件 fiber: 通过 currentlyRenderingFiber 变量记录当前组件 fiber

  3. 选择对应阶段的 Hook: mount 阶段选择 HooksDispatcherOnMount 作为 Hooks Map, update 阶段选择 HooksDispatcherOnUpdate 作为 Hooks Map函数组件mount 阶段 创建 hook 数据结构, 存储到 fiber.memoizedState 或者 hook.next, 初次建立其 hooksfiber 之间的关系。函数组件 update 阶段 找到当前函数组件 fiber, 找到当前 hook, 更新 hook 状态。

  4. 执行函数组件: 执行我们真正函数组件,所有的 hooks 将依次执行, 每个 hook 内部要根据 currentlyRenderingFiber 读取对应的内容,

  5. 重置状态: 执行完 Component, 立马重置 currentlyRenderingFiber, 防止函数组件外部调用。置当前正在处理的 hook、当前正在使用的 hook

3.1 /packages/react-reconciler/fiberHooks.js

export function renderWithHooks(workInProgress, Component, lane) {
renderLane = lane;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;

const current = workInProgress.alternate;

if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}

const props = workInProgress.pendingProps;
const children = Component(props);

currentHook = null;
renderLane = NoLane;
workInProgressHook = null;
currentlyRenderingFiber = null;

return children;
}

3.2 /packages/react-reconciler/fiberHooks.js

/**
* @description: Mount 阶段 Hooks 实现
*/
const HooksDispatcherOnMount = {
use,
useRef: mountRef,
useMemo: mountMemo,
useState: mountState,
useEffect: mountEffect,
useContext: readContext,
useCallback: mountCallback,
useTransition: mountTransition
};

/**
* @description: Update 阶段 Hooks 实现
*/
const HooksDispatcherOnUpdate = {
use,
useRef: updateRef,
useMemo: updateMemo,
useState: updateState,
useEffect: updateEffect,
useContext: readContext,
useCallback: updateCallback,
useTransition: updateTransition
};

四、函数组件 mount 阶段 Hook


useEffectmount 阶段, 执行 mountWorkInProgressHook 创建新的 hook 数据结构, 存储到 fiber.memoizedState 或者 hook.next, 与当前函数组件 fiber 建立联系, 并更新记录当前正在处理的 hook 变量 workInProgressHook。随后标记 fiber.flags, 标记 effect.tagPassive | HookHasEffect。根据 useEffect 传入的 createdeps 创建 effect 副作用数据结构, 存入 hook.memoizedState, 存入 fiber.updateQueue 组成副作用链表。

标记逻辑如下:

  1. 标记 fiber.flagsPassiveEffect: 表示当前 fiber 本次更新存在副作用,该副作用是 useEffect 还是 useLayout 通过 effect.tag 来决定。

  2. 标记 effect.tagPassive | HookHasEffect: 表示当前 effectuseEffect, 并且本次更新存在副作用

effect.next 环形链表逻辑为: useEffect hook 同其他 hook 形成一个单向链表, 存储在 fiber.memoizedState 中。但是为了方便遍历 useEffect 副作用链表, 为 effect 增加 next , 用于指向下一个 useEffecteffect, 形成环状链表。因此, 在遍历时, 只需要取到其中一个 useEffect, 就可以通过 effect.next 拿到所有 useEffect

4.1 mountEffect

function mountEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= PassiveEffect;
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
undefined,
nextDeps
);
}

4.2 mountWorkInProgressHook

function mountWorkInProgressHook() {
const hook = {
next: null,
baseQueue: null,
baseState: null,
updateQueue: null,
memoizedState: null
};

if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}

4.3 pushEffect

function pushEffect(hookFlags, create, destroy, deps) {
const effect = {
deps,
create,
destroy,
next: null,
tag: hookFlags
};

const fiber = currentlyRenderingFiber;
const updateQueue = fiber.updateQueue;
if (updateQueue == null) {
const updateQueue = createFCUpdateQueue();
fiber.updateQueue = updateQueue;
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const lastEffect = updateQueue.lastEffect;
if (lastEffect == null) {
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
}
}
return effect;
}

五、函数组件 update 阶段 Hook


useEffectupdate 阶段, 执行 updateWorkInProgressHook, 从 currentlyRenderingFiber 记录当前函数组件 fiber 中取出对应的 hook。取出时是判断当前正在使用的 hook 变量 currentHook 是否存在。如果不存在, 说明当前 hookhooks 链表的第一个, 所以从 fiber.memoizedState 中取; 如果存在, 直接从 currentHook.next 中取。得到当前 hook 后, 复制当前 hook 形成新的 hooks 链表关系, 更新记录当前使用的 fiber 变量 currentHook 与记录当前正在处理的 fiber 变量 workInProgressHook

由上可以看出, mount 阶段hookupdate 阶段hook 都非常依赖 workInProgressHookcurrentHook 记录、传递 hooks 链表中的当前正在处理、或者使用的 hook, 因此, 必须保证 mount 阶段update 阶段 阶段的 hook 执行顺序一致。

随后, 根据 updateWorkInProgressHook 得到新的 hook, 根据用户传入的 deps 依赖与之前的 deps 对比, 如果前后无变化, 更新如下:

  1. 更新 effect.tagPassive, 表示当前 effectuseEffect, 并且本次更新没有副作用

  2. 更新 hook.memoizedState

如果前后 deps 有变化, 更新如下:

  1. 更新 fiber.flagsPassiveEffect: 表示当前 fiber 本次更新存在副作用。该副作用是 useEffect 还是 useLayout 通过 effect.tag 来决定。

  2. 更新 effect.tagPassive | HookHasEffect: 表示当前 effectuseEffect, 并且本次更新存在副作用

  3. 更新 hook.memoizedState

5.1 updateEffect

function updateEffect(create, deps) {
let destroy;
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(Passive, create, destroy, nextDeps);
return;
}
}

currentlyRenderingFiber.flags |= PassiveEffect;
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
destroy,
nextDeps
);
}
}

5.2 updateWorkInProgressHook

function updateWorkInProgressHook() {
let nextCurrentHook = null;

if (currentHook == null) {
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}

if (nextCurrentHook == null) {
/**
* @description: nextCurrentHook 为 null
* 之前: u1 u2 u3
* 现在: u1 u2 u3 u4
* 原因:
* 1. if(){ u4 } => u4 放在了 If 语句中
*/
throw new Error('本次执行时的 Hook 比上次执行时多');
}

currentHook = nextCurrentHook;

const newHook = {
next: null,
baseState: currentHook.baseQueue,
baseQueue: currentHook.baseState,
updateQueue: currentHook.updateQueue,
memoizedState: currentHook.memoizedState
};

if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}

5.3 pushEffect

function pushEffect(hookFlags, create, destroy, deps) {
const effect = {
deps,
create,
destroy,
next: null,
tag: hookFlags
};

const fiber = currentlyRenderingFiber;
const updateQueue = fiber.updateQueue;
if (updateQueue == null) {
const updateQueue = createFCUpdateQueue();
fiber.updateQueue = updateQueue;
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const lastEffect = updateQueue.lastEffect;
if (lastEffect == null) {
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
}
}
return effect;
}

5.4 areHookInputsEqual

function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null || nextDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(prevDeps[i], nextDeps[i])) {
continue;
}
return false;
}
return true;
}

六、completeWork 阶段收集标记


completeWork 阶段是一个向上归并的过程, 将子 fiber.flags 冒泡到父 fiber.flags 中, 最终会冒泡到 root.finishedWork.flags 或者 root.finishedWork.subtreeFlags

七、commit 阶段根据标记处理副作用


render 阶段,实际没有进行真正的 DOM 元素的增加,删除,React 把想要做的不同操作打成不同的 effectTag ,等到 commit 阶段,统一处理这些副作用,包括 DOM 元素增删改,执行一些生命周期等。hooks 中的 useEffectuseLayoutEffect 也是副作用。其中, useEffect 处理逻辑如下:

  1. 调度副作用: 判断 root.finishedWork.flags 或者 root.finishedWork.subtreeFlags 是否存在 PassiveMask 标记, 如果存在, 则通过 scheduleCallback 调度 useEffect 副作用

  2. 收集副作用: 通过 root.pendingPassiveEffects 来存储 updateunmount 两种情况的副作用

    1. commit mutation 挂载阶段: 如果 root.finishedWork.flags 存在 PassiveEffect, 开始收集 update 副作用回调, 存储到 root.pendingPassiveEffectsupdate 中。

    2. commit mutation Deletion 删除阶段: 如果 root.finishedWork.flags 存在 PassiveEffect, 开始收集 unmount 副作用回调, 存储到 root.pendingPassiveEffectsunmount 中。

  3. 执行副作用:

    1. 遍历 root.pendingPassiveEffects.unmount, 执行所有 useEffectdestroy 回调: 本次更新的任何 create 回调都必须在所有上一次更新的 destroy 回调执行完后在执行。ReactuserEffect 在执行当前 effect 之前会对上一个 effect 进行清除。

    2. 遍历 root.pendingPassiveEffects.update, 执行所有 useEffect 并且带有副作用标记的 destroy 回调: 本次更新的任何 create 回调都必须在所有上一次更新的 destroy 回调执行完后在执行。ReactuserEffect 在执行当前 effect 之前会对上一个 effect 进行清除。

    3. 遍历 root.pendingPassiveEffects.update, 执行所有 useEffect 并且带有副作用标记的 create 回调: 本次更新的任何 create 回调都必须在所有上一次更新的 destroy 回调执行完后在执行

  4. 处理更新流程: 在 useEffectcreate 回调中, 可能会有 setState 等触发新的更新的操作, 所以等 destroycreate 回调执行完毕后继续处理更新流程。

整体逻辑如下所示:

function flushPassiveEffects(pendingPassiveEffects) {
let didFlushPassiveEffect = false;

pendingPassiveEffects.unmount.forEach(effect => {
didFlushPassiveEffect = true;
commitHookEffectListUnmount(Passive, effect);
});
pendingPassiveEffects.unmount = [];
pendingPassiveEffects.update.forEach(effect => {
didFlushPassiveEffect = true;
commitHookEffectListDestroy(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update.forEach(effect => {
didFlushPassiveEffect = true;
commitHookEffectListCreate(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update = [];
flushSyncCallbacks();
return didFlushPassiveEffect;
}

function commitRoot(root) {
const finishedWork = root.finishedWork;

if (finishedWork == null) {
return;
}

const lane = root.finishedLane;

if (lane === NoLane) {
console.log('commitRoot root.finishedLane 不应该是 NoLane');
}

root.finishedWork = null;
root.finishedLane = NoLane;
markRootFinished(root, lane);

if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
) {
if (!rootDoesHasPassiveEffects) {
rootDoesHasPassiveEffects = true;
scheduleCallback(NormalPriority, () => {
flushPassiveEffects(root.pendingPassiveEffects);
return;
});
}
}

/**
* @description: 判断是否存在三个子阶段需要执行的操作
*/
const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
const subtreeHasEffects =
(finishedWork.subtreeFlags & MutationMask) !== NoFlags;

if (rootHasEffect || subtreeHasEffects) {
commitMutationEffects(finishedWork, root);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root);
} else {
root.current = finishedWork;
}

rootDoesHasPassiveEffects = false;
ensureRootIsScheduled(root);
}

八、问题


8.1 useEffect 与 useLayoutEffect 有什么区别呢?

useEffectcommit 阶段完成以后 (真实 DOM 挂载完成以后) 通过 scheduleCallback 异步调度执行, 不会阻塞浏览器渲染。而 useLayoutEffectcommit 阶段 (先于 DOM 挂载) 同步执行, 会阻塞浏览器渲染。伪代码如下所示:

commitRoot()
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
spawnedLane: Lane,
) {
try {
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority,
spawnedLane,
);
} finally {
}

return null;
}
commitRootImpl()
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
) {
do {
// commit 阶段首先执行 useLayoutEffect
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
……
}
if(){
……
}else {
……
}
root.finishedWork = null;
root.finishedLanes = NoLanes;

if (finishedWork === root.current) {
……
}

markRootFinished(root, remainingLanes, spawnedLane);

if (root === workInProgressRoot) {
……
} else {
……
}

// useEffect 通过 scheduleCallback 异步调度执行
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}


const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;

if (subtreeHasEffects || rootHasEffect) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);

const prevExecutionContext = executionContext;
executionContext |= CommitContext;

ReactCurrentOwner.current = null;

const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);

if (enableProfilerTimer) {
recordCommitTime();
}

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
rootCommittingMutationOrLayoutEffects = root;
}

// DOM 挂载、卸载、useEffect 副作用收集 操作
commitMutationEffects(root, finishedWork, lanes);
}

8.2 为什么每次更新的时候都要运行 useEffect ?

避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug