跳到主要内容

认识

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

一、认识


useLayoutEffect 初始渲染或者状态更新之后, 在 commit 阶段中同步执行, 同步执行时阻塞浏览器渲染。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


五、函数组件 update 阶段 Hook


六、沉淀与思考


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);
}