前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正
博客链接:pionpill
对应源码: github.com/facebook/re…
react18 中,副作用是指 useState 钩子导致的副作用,可以被分为以下几种:
- 卸载副作用(passive unmount effects):在组件被卸载时执行的副作用,比如取消事件订阅、释放资源等。
- 挂载副作用(passive mount effects): 在组件被挂载时执行的副作用,比如订阅事件、初始化资源等。
- 更新副作用(passive update effects): 在组件更新时执行的副作用,但不会触发组件重新渲染。
我们要知道 react 副作用的执行是异步的,看下面这段代码:
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
Schedular 将 flushPassiveEffects 的执行放在了 NormalSchedulerPriority 中,这是一个低优先级,让浏览器有空了就执行一下副作用处理函数。但很多情况下我们的方法走到某处需要保证副作用已经执行完了,这时候就会在代码中强制执行 flushPassiveEffects。
这个方法非常非常的重要,直接关系到我们写 useEffect 等副作用钩子(✨约3174行):
export function flushPassiveEffects(): boolean {
// rootWithPendingPassiveEffects 用于跟踪具有待处理效应的根节点,这个变量的赋值会在后续操作中进行
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects;
// 优先级处理,因为这个方法可能从很多地方调用(暂时不管)
const remainingLanes = pendingPassiveEffectsRemainingLanes;
pendingPassiveEffectsRemainingLanes = NoLanes;
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
const prevTransition = ReactCurrentBatchConfig.transition;
const previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(priority);
// 核心方法
return flushPassiveEffectsImpl();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
releaseRootPooledCache(root, remainingLanes);
}
}
return false;
}
核心方法在 flushPassiveEffectsImpl 里面,其他都是异步优先级处理(✨约3226行):
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
const transitions = pendingPassiveTransitions;
pendingPassiveTransitions = null;
const root = rootWithPendingPassiveEffects;
const lanes = pendingPassiveEffectsLanes;
rootWithPendingPassiveEffects = null;
// 卸载副作用与钩子
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
flushSyncWorkOnAllRoots();
return true;
}
commitPassiveUnmountOnFiber
在原生 DOM 中,如果我们仅删除了 DOM 结构,却没有手动释放一些点击,悬浮等事件的回调函数,这些方法仍然会存在于内存中。因此,react 必须保证删除 DOM 的同时将相关的卸载副作用删除
commitPassiveUnmountEffects 方法用于卸载副作用,最终是调用了 commitPassiveUnmountOnFiber 方法(✨约4240行):
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
if (finishedWork.flags & Passive) {
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive | HookHasEffect,
);
}
break;
}
case OffscreenComponent: {
// 这是一个开发中的 API,我们暂时不管这里的逻辑
}
default: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
break;
}
}
}
recursivelyTraversePassiveUnmountEffects
所有类型的节点都会调用 recursivelyTraversePassiveUnmountEffects 方法,该方法遍历要删除的节点及其子节点,卸载无用的副作用(✨约4207行)。
function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
// 获取要删除的子节点
const deletions = parentFiber.deletions;
if ((parentFiber.flags & ChildDeletion) !== NoFlags && deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
nextEffect = childToDelete;
// 删除卸载副作用
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
childToDelete,
parentFiber,
);
}
detachAlternateSiblings(parentFiber);
}
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
// 处理子节点
commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}
}
commitHookEffectListUnmount
另外还有一个 commitHookPassiveUnmountEffects 方法,这个方法只会在节点类型为 FunctionComponent, ForwardRef, SimpleMemoComponent 时调用。commitHookPassiveUnmountEffects 方法最终会调 commitHookEffectListUnmount 方法卸载一些钩子(✨约567行):
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 inst = effect.inst;
const destroy = inst.destroy; // 卸载副作用的方法
if (destroy !== undefined) {
inst.destroy = undefined;
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
commitPassiveMountOnFiber
commitPassiveMountOnFiber 方法用于执行一些副作用,最终是调用了 commitPassiveMountOnFiber 方法,这个方法逻辑上和 commitPassiveUnmountOnFiber 类似,不同 tag 类型的 FiberNode 处理过程略有不同(✨约3563行):
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
commitHookPassiveMountEffects(
finishedWork,
HookPassive | HookHasEffect,
);
}
break;
}
case xxx: {
// 省略不同 tag 处理过程
}
default: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
break;
}
}
}
recursivelyTraversePassiveMountEffects
recursivelyTraversePassiveMountEffects 这个方法会遍历子节点并调用 commitPassiveMountOnFiber 方法(✨约3540行):
function recursivelyTraversePassiveMountEffects(
root: FiberRoot,
parentFiber: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
) {
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveMountOnFiber(
root,
child,
committedLanes,
committedTransitions,
);
child = child.sibling;
}
}
}
commitHookEffectListMount
然后我们看一下 commitHookPassiveMountEffects 方法,这个方法会执行具体的副作用(✨约3363行):
function commitHookPassiveMountEffects(
finishedWork: Fiber,
hookFlags: HookFlags,
) {
try {
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
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;
const inst = effect.inst;
// 执行副作用
const destroy = create();
// 返回 destroy,方便卸载时清理
inst.destroy = destroy;
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
在 commitHookEffectListMount 方法中,我们执行了副作用并返回一个 destroy,方便组件卸载时卸载这些副作用。
这里有一个非常非常非常关键的东西: updateQueue(这里简单讲一下,后面会单独讲他),它是我们函数组件的状态更新队列,先看一下它的数据结构(✨约240行):
export type FunctionComponentUpdateQueue = {
// 注意想最后一个 Effect 的指针
lastEffect: Effect | null,
// 存储组件相关的事件处理函数的 payload
events: Array<EventFunctionPayload<any, any, any>> | null,
// 存储组件相关的状态管理库一致性检查函数
stores: Array<StoreConsistencyCheck<any>> | null,
// memo 的缓存值
memoCache?: MemoCache | null,
};
这里面比较重要的就是 Effect 对象(✨约214行):
export type Effect = {
// 一个标记,表示 effect 的类型
tag: HookFlags,
// 用于创建 Effect 函数,返回一个 destroy 清理函数
create: () => (() => void) | void,
// 实例,用来存储额外数据的,目前仅会存储 destroy 函数
inst: EffectInstance,
// effect 依赖项,依赖发生关系时,effect 重新执行
deps: Array<mixed> | null,
// 指向下一个 effect
next: Effect,
};
简单来说 commitHookEffectListMount 方法会遍历所有的 effect 并执行,并且将执行后返回的 destroy 方法挂载到 inst 属性上,方便以后卸载。
这里简单看一下 updateQueue,后面讲钩子的时候会着重分析他。
flushSyncWorkOnAllRoots
flushSyncWorkOnAllRoots 方法会在最后执行,它用于在所有的根节点上同步执行未完成的工作(✨约142行):
export function flushSyncWorkOnAllRoots() {
flushSyncWorkAcrossRoots_impl(false);
}
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
if (isFlushingWork || !mightHavePendingSyncWork) {
return;
}
let didPerformSomeWork;
let errors: Array<mixed> | null = null;
isFlushingWork = true;
do {
didPerformSomeWork = false;
let root = firstScheduledRoot;
while (root !== null) {
if (onlyLegacy && root.tag !== LegacyRoot) {
// Skip non-legacy roots.
} else {
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes =
getWorkInProgressRootRenderLanes();
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (includesSyncLane(nextLanes)) {
// 存在同步任务
try {
didPerformSomeWork = true;
// 执行同步任务逻辑
performSyncWorkOnRoot(root, nextLanes);
} catch (error) {
errors === null ? errors = [error] : errors.push(error);
}
}
}
root = root.next;
}
} while (didPerformSomeWork);
isFlushingWork = false;
// 错误处理
if (errors !== null) {
if (errors.length > 1) {
if (typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
throw new AggregateError(errors);
} else {
for (let i = 1; i < errors.length; i++) {
scheduleImmediateTask(throwError.bind(null, errors[i]));
}
const firstError = errors[0];
throw firstError;
}
} else {
const error = errors[0];
throw error;
}
}
}
这个方法核心逻辑是检测到有同步任务要执行的时候,执行同步任务,很好理解。副作用怎么会产生同步任务呢?比如 useLayoutEffect 会在 DOM 更新前同步执行,因此产生了同步任务。
好,最后整理一下 flushPassiveEffects 的逻辑: