认识
一、认识
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
存储 useEffect
的 effect
副作用链表
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
存储 useEffect
的 effect
副作用链表
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.js
的 beginWork
阶段, 遇到 FunctionComponent
类型的 Fiber
, 会执行 updateFunctionComponent
。updateFunctionComponent
内部调用 renderWithHooks
。主要工作如下:
-
重置状态: 函数组件执行前, 重置当前正在处理的
hook
、当前正在使用的hook
-
记录当前组件
fiber
: 通过currentlyRenderingFiber
变量记录当前组件fiber
-
选择对应阶段的
Hook
:mount
阶段选择HooksDispatcherOnMount
作为Hooks Map
,update
阶段选择HooksDispatcherOnUpdate
作为Hooks Map
。 函数组件mount
阶段 创建hook
数据结构, 存储到fiber.memoizedState
或者hook.next
, 初次建立其hooks
与fiber
之间的关系。函数组件update
阶段 找到当前函数组件fiber
, 找到当前hook
, 更新hook
状态。 -
执行函数组件: 执行我们真正函数组件,所有的
hooks
将依次执行, 每个hook
内部要根据currentlyRenderingFiber
读取对应的内容, -
重置状态: 执行完
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
useEffect
在 mount
阶段, 执行 mountWorkInProgressHook
创建新的 hook
数据结构, 存储到 fiber.memoizedState
或者 hook.next
, 与当前函数组件 fiber
建立联系, 并更新记录当前正在处理的 hook
变量 workInProgressHook
。随后标记 fiber.flags
, 标记 effect.tag
为 Passive | HookHasEffect
。根据 useEffect
传入的 create
和 deps
创建 effect
副作用数据结构, 存入 hook.memoizedState
, 存入 fiber.updateQueue
组成副作用链表。
标记逻辑如下:
-
标记
fiber.flags
为PassiveEffect
: 表示当前fiber
本次更新存在副作用,该副作用是useEffect
还是useLayout
通过effect.tag
来决定。 -
标记
effect.tag
为Passive | HookHasEffect
: 表示当前effect
为useEffect
, 并且本次更新存在副作用
effect.next
环形链表逻辑为: useEffect hook
同其他 hook
形成一个单向链表, 存储在 fiber.memoizedState
中。但是为了方便遍历 useEffect
副作用链表, 为 effect
增加 next
, 用于指向下一个 useEffect
的 effect
, 形成环状链表。因此, 在遍历时, 只需要取到其中一个 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
useEffect
在 update
阶段, 执行 updateWorkInProgressHook
, 从 currentlyRenderingFiber
记录当前函数组件 fiber
中取出对应的 hook
。取出时是判断当前正在使用的 hook
变量 currentHook
是否存在。如果不存在, 说明当前 hook
是 hooks
链表的第一个, 所以从 fiber.memoizedState
中取; 如果存在, 直接从 currentHook.next
中取。得到当前 hook
后, 复制当前 hook
形成新的 hooks
链表关系, 更新记录当前使用的 fiber
变量 currentHook
与记录当前正在处理的 fiber
变量 workInProgressHook
。
由上可以看出, mount
阶段 的 hook
和 update
阶段 的 hook
都非常依赖 workInProgressHook
和 currentHook
记录、传递 hooks
链表中的当前正在处理、或者使用的 hook
, 因此, 必须保证 mount
阶段 和 update
阶段 阶段的 hook
执行顺序一致。
随后, 根据 updateWorkInProgressHook
得到新的 hook
, 根据用户传入的 deps
依赖与之前的 deps
对比, 如果前后无变化, 更新如下:
-
更新
effect.tag
为Passive
, 表示当前effect
为useEffect
, 并且本次更新没有副作用 -
更新
hook.memoizedState
如果前后 deps
有变化, 更新如下:
-
更新
fiber.flags
为PassiveEffect
: 表示当前fiber
本次更新存在副作用。该副作用是useEffect
还是useLayout
通过effect.tag
来决定。 -
更新
effect.tag
为Passive | HookHasEffect
: 表示当前effect
为useEffect
, 并且本次更新存在副作用 -
更新
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
中的 useEffect
和 useLayoutEffect
也是副作用。其中, useEffect
处理逻辑如下:
-
调度副作用: 判断
root.finishedWork.flags
或者root.finishedWork.subtreeFlags
是否存在PassiveMask
标记, 如果存在, 则通过scheduleCallback
调度useEffect
副作用 -
收集副作用: 通过
root.pendingPassiveEffects
来存储update
和unmount
两种情况的副作用-
在
commit mutation
挂载阶段: 如果root.finishedWork.flags
存在PassiveEffect
, 开始收集update
副作用回调, 存储到root.pendingPassiveEffects
的update
中。 -
在
commit mutation Deletion
删除阶段: 如果root.finishedWork.flags
存在PassiveEffect
, 开始收集unmount
副作用回调, 存储到root.pendingPassiveEffects
的unmount
中。
-
-
执行副作用:
-
遍历
root.pendingPassiveEffects.unmount
, 执行所有useEffect
的destroy
回调: 本次更新的任何create
回调都必须在所有上一次更新的destroy
回调执行完后在执行。React
的userEffect
在执行当前effect
之前会对上一个effect
进行清除。 -
遍历
root.pendingPassiveEffects.update
, 执行所有useEffect
并且带有副作用标记的destroy
回调: 本次更新的任何create
回调都必须在所有上一次更新的destroy
回调执行完后在执行。React
的userEffect
在执行当前effect
之前会对上一个effect
进行清除。 -
遍历
root.pendingPassiveEffects.update
, 执行所有useEffect
并且带有副作用标记的create
回调: 本次更新的任何create
回调都必须在所有上一次更新的destroy
回调执行完后在执行
-
-
处理更新流程: 在
useEffect
的create
回调中, 可能会有setState
等触发新的更新的操作, 所以等destroy
、create
回调执行完毕后继续处理更新流程。
整体逻辑如下所示:
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 有什么区别呢?
useEffect
在 commit
阶段完成以后 (真实 DOM
挂载完成以后) 通过 scheduleCallback
异步调度执行, 不会阻塞浏览器渲染。而 useLayoutEffect
在 commit
阶段 (先于 DOM
挂载) 同步执行, 会阻塞浏览器渲染。伪代码如下所示:
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;
}
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