React 源码揭秘 | Effect更新流程
前面的文章介绍了 hooks和commit流程,算是前置知识,这篇来讨论一下useEffect的原理。
useEffect用来处理副作用,比如网络请求,dom操作等等, 其本质也是个hooks,包含hooks的memorizedState, updateQueue, next
Effect对象
useEfffect这个hooks的memorizedState存储的是Effect对象,其ts定义为
/** 定义effect */
export interface Effect {
tags: HookEffectTag;
// 依赖数组
deps: HookDeps;
// 传入的创建effect
create: EffectCallback | null;
// 清除effect
destory: EffectCallback | null;
// next 用来连接在updateQueu中的Effect对象,Effect存在hook.memorizedState和fiber.updateQueue上
next: Effect;
}
其中,tags定义为HookEffectTag,其为数字类型,定义在hookEffectTag.ts
export const Passive = 0b0010;
// 当前hook存在effect需要处理
export const HookHasEffect = 0b0001;
export type HookEffectTag = number;
其中,Passive表示当前存在useEffect,而HookHasEffect表示当前useEffect在本次更新中需要处理。
deps就是依赖数组,类比useMemo useCallback
create是useEffect传入的create函数
destory是create函数返回的销毁函数
next表示下一个Effect对象
虽然useEffect本身也是个Hook,但是其hook节点的updateQueue是没有被使用的。useEffect将其Effect对象保存在memorizedState,并且使用了当前渲染Fiber上的updateQueue来存储Effect链,结构如下:
FCUpdateQueue
其中,对于函数类型的Fiber,其updateQueue的作用就是存储当前函数组件挂载的Effect对象,其使用的是FCUpdateQueue 是专门给函数组件提供呢更新队列,其继承updateQueue,多了lastEffect指针,指向Effect链(其本质也是环)。如下:
/** 函数组件专用的UpdateQueue增加了lastEffect 指向当前收集到的Effect */
export class FCUpdateQueue<State> extends UpdateQueue<State> {
public lastEffect: Effect | null = null;
}
useEffect工作原理
每次更新的时候,mountEffect或者updateEffect都会检查当前的Effect是否被执行
- 如果需要被执行,则将其放到Fiber对象的updateQueue中,并且将其tag设置为Passive|HookHasEffect
- 如果不需要被执行,也需要将其推入updateQueue,只是其tag设置为Passive 代表有副作用,但是不执行
给当前的Fiber节点,设置PassiveEffect的flag
当commitMutation阶段时,会检查节点的Flag,如果包含passiveEffect,就会将当前节点的updateQueue推入root.pendingPassiveEffect数组中,你当前可以直接的将其理解为一个数组
在commitPassive阶段,会读取root.pendingPassiveEffect,依次执行FCUpdateQueue的内容
下面我们来看mount/updateEffect函数做了什么
mountEffect
挂载阶段,useEffect本质上执行的是mountEffect,其步骤如下:
1. 获取当前hook对象
2. 给当前的currentRenderingFiber.flag 设置PassiveEffect 表示当前Fiber对象包含Effect
3. 创建Effect对象,保存进creat函数和tag,并且将其分别保存到memorizedState和fiber.updateQueue
实现如下:
/** 挂载Effect */
function mountEffect(
create: EffectCallback,
deps: HookDeps
): EffectCallback | void {
/** effect 在hook中的存储方式是:
* hook:
* memorizedState = Effect
* updateQueue = null
* next = nextHook
* fiber:
* updateQueue -> Effect1 -next-> Effect2 -...
*/
// 获取到hook
const hook = mountWorkInProgressHook();
// 给fiber设置PassiveEffect 表示存在被动副作用
(currentRenderingFiber as FiberNode).flags |= PassiveEffect;
hook.memorizedState = pushEffect(
// 初始化状态下,所有的useEffect都执行,所以这里flag设置为 Passive|HookHasEffect
Passive | HookHasEffect,
create,
null,
deps
);
}
为什么要设置 fiber.flags?
因为effect真正执行的阶段在commit的passive阶段,而其收集阶段在commit的mutation阶段,所以render阶段时,需要在fiber上设置PassiveMask标记,表示当前fiber节点有副作用需要处理,commit阶段才会收集!
需要注意的是,mount阶段的tags为Passive | HookHasEffect 代表挂载阶段的所有Effect都会线执行一次!
其中,创建Effect和挂载到fiber.updateQueue的操作是pushEffect完成的!
pushEffect
pushEffect函数创建一个Effect对象,并且为其设置mask create destory deps依赖这些参数,可以将其看成是Effect对象的构造器。
同时,pushEffect还会将创建的Effect对象挂载到当前渲染Fiber的updateQueue上 并且返回这个Effect对象,实现如下:
/** 创建Effect对象,把effect加入到fiber.updateQueue 并且返回创建的Effect */
function pushEffect(
tags: Flags,
create: EffectCallback | null,
destory: EffectCallback | null,
deps: HookDeps
) {
const effect: Effect = {
tags,
create,
destory,
deps: deps === undefined ? null : deps,
next: null,
};
const updateQueue = currentRenderingFiber.updateQueue;
if (!updateQueue || !(updateQueue instanceof FCUpdateQueue)) {
// 创建一个FCUpdateQueue
const fcUpdateQueue = new FCUpdateQueue<Effect>();
effect.next = effect; // 构建环
fcUpdateQueue.lastEffect = effect;
currentRenderingFiber.updateQueue = fcUpdateQueue;
} else {
// 已经存在 FCUpdateQueue 添加 后加环
const fcUpdateQueue =
currentRenderingFiber.updateQueue as FCUpdateQueue<Effect>;
if (fcUpdateQueue.lastEffect) {
effect.next = fcUpdateQueue.lastEffect.next;
fcUpdateQueue.lastEffect.next = effect;
fcUpdateQueue.lastEffect = effect;
}
}
return effect;
}
pushEffect会检查,如果currentRenderingFiber.updateQueue为null (在renderWithHook时,会将updateQueue置为空以重置Effect) 则先创建一个FCUpdateQueue再加入,如果已经存在就直接加入。
updateEffect
更新阶段,由于fiber.updateQueue被清空,所以需要根据hook.memorizedState存储的Effect对象信息,重新判断哪些Effect需要重新执行。
判断是否要重新加入的依据就是deps,Effect存储了上一次的deps,updateEffect比较当前的deps和上一次prevDeps是否相等来判断是否需要重新执行。
判断方式为areInputDepsEqual, 前面有讲过。
- 如果需要执行,则把Effect的tags改成 Passive | HookHasEffect 推入updateQueue
- 如果不需要执行,则把Effect的tags改成Passive并且推入updateQueue
同时,在mount阶段,由于create函数还没有运行,所以无法获得destory函数,但是子啊update阶段,create函数已经在Passive Commit阶段运行过了,commit阶段会将create的返回值destory存入Effect对象中,直接获取即可,
也就是说,不论是mount还是update阶段,所有的Effect都会被挂载fiber.updateQueue上,其是否执行通过Effect.tag是否包含HookHasEffect这个HookEffectTag判断!
注意,更新阶段如果有需要执行的effect,也需要吧fiber.flags merge passiveEffect
实现如下:
/** 更新Effect */
function updateEffect(
create: EffectCallback,
deps: HookDeps
): EffectCallback | void {
// 获取当前hook
const hook = updateWorkInProgressHook();
const prevDeps = hook.memorizedState.deps;
const destory = hook.memorizedState.destory;
if (areHookInputsEqual(prevDeps, deps)) {
// 相等 pushEffect 并且设置tag为Passive 被动副作用
hook.memorizedState = pushEffect(
Passive,
create,
// 前一个副作用hook的destory
destory,
deps
);
} else {
/** 不等 表示hook有Effect */
hook.memorizedState = pushEffect(
Passive | HookHasEffect, // 注意这里是 Passive 是Effect的tag 区分fiber的tag PassiveEffect
create,
// 前一个副作用hook的destory
destory,
deps
);
}
(currentRenderingFiber as FiberNode).flags |= PassiveEffect;
}
Effect收集阶段
effect的收集在commit阶段完成,我们上篇说了,如果某个Fiber节点存在需要执行的Effect,则其PassiveEffect的flag就会在completeWork阶段被冒泡到root节点。
commitRoot阶段,通过判断root节点是否有PassiveEffect的flags或subTreeFlags来判断是否开启Passive流程!
/** 设置调度 执行passiveEffect */ty
/** 真正执行会在commit之后 不影响渲染 */
/** commit阶段会收集effect到root.pendingPassiveEffect */
// 有删除 或者收集到Passive 都运行
if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subTreeFlags & PassiveMask) !== NoFlags
) {
// 调度副作用
scheduler.scheduleCallback(
PriorityLevel.NORMAL_PRIORITY,
flushPassiveEffect.bind(null, root.pendingPassiveEffects)
);
}
此时root.pendingPassiveEffect为空,需要在Mutation阶段收集
export const commitMutationEffects = commitEffect(
"mutation",
MutationMask | PassiveMask,
commitMutationEffectsOnFiber
);
可以看到commitMutationEffects中 mash包含了PassiveMask
pendingPassiveEffect的结构
root.pendingPassiveEffect的结构如下:
export interface PendingPassiveEffect {
// 更新的effect
update: Effect[];
// 卸载的effect
unmount: Effect[];
}
可以看到,其包含update和unmount两个数组,分别存储本次更新收集到的需要更新的effect和需要卸载的effect,在每次Passive Commit阶段接诉后,pendingPassEffect都会被置空!
收集的几个地方
通过pendingPassEffect的结构就能看出,Effect的收集阶段分别发生在
1. 函数组件的卸载阶段
2. 函数组件的更新阶段
副作用的收集由commitPassiveEffect完成,其定义如下:
/** 收集被动副作用,这个函数可能会在
* 1. commitMutationEffectsOnFiber调用
* 2. 在delection时调用
*/
function commitPassiveEffect(
fiber: FiberNode,
root: FiberRootNode,
type: "update" | "unmount"
) {
if (fiber.tag !== FunctionComponent) return;
if (type === "update" && (fiber.flags & PassiveEffect) === NoFlags) return;
const fcUpdateQueue = fiber.updateQueue as FCUpdateQueue<Effect>;
if (fcUpdateQueue && fcUpdateQueue.lastEffect) {
// 收集effect
root.pendingPassiveEffects[type].push(fcUpdateQueue.lastEffect);
}
}
可以看到,其逻辑就是把当前存在PassiveEffect的Fiber节点的FCUpdateQueue.lastEffect 存入对应的update unmount数组,对于卸载阶段,是允许其flag不存在PassiveEffect的,因为卸载函数组件要destory其所有的effect
在commitMutationEfffectOnFiber 中,调用次函数,用来收集update阶段的effect
if ((flags & PassiveEffect) !== NoFlags) {
// 存在被动副作用
commitPassiveEffect(finishedWork, root, "update");
}
在commitDeletion阶段中,调用次函数用来收集unmount阶段的effect
if (childToDelete.tag === FunctionComponent) {
/** 函数组件的情况下,需要收集Effect */
commitPassiveEffect(childToDelete, root, "unmount");
}
在完成了Mutation Commit阶段之后,root.pendingPassiveEffect 就分别收集到了更新和卸载阶段的FCUpdateQueue
Effect的执行
effect的执行发生在Passive Commit阶段,我们前面说了,在Mutation阶段开始之前,会把一个flushPassiveEffect函数交给scheduler调度,以便在Mutation之后异步执行,其实现如下
function flushPassiveEffect(pendingPassiveEffect: PendingPassiveEffect) {
// 处理卸载 把所有的Passive flag的effect都执行destor
pendingPassiveEffect.unmount.forEach((unmountEffect) => {
commitHookEffectListUnmount(Passive, unmountEffect);
});
pendingPassiveEffect.unmount = [];
// 处理update 的destory flag为Passive|HookHasEffect
pendingPassiveEffect.update.forEach((updateEffect) => {
commitHookEffectListDestory(Passive | HookHasEffect, updateEffect);
});
// 处理update的create flag为Passive| HookHasEffect
pendingPassiveEffect.update.forEach((updateEffect) => {
commitHookEffectListCreate(Passive | HookHasEffect, updateEffect);
});
pendingPassiveEffect.update = [];
}
其实本质就是按顺序处理pendingPasiveEffect
首先处理卸载的effect -> 再处理更新的effect的destory -> 再处理更新effect的create
最后清空pendingPassiveEffect 以方便下次收集
我们首先看更新阶段处理create 其定义在commitHookEffectListCreate
function commitHookEffectList(
flags: HookEffectTag,
lastEffect: Effect | null,
callback: (effect: Effect) => void
) {
let currentEffect = lastEffect.next;
do {
if ((flags & currentEffect.tags) === flags) {
// flag必须完全相等 执行callback
callback(currentEffect);
}
currentEffect = currentEffect.next;
} while (currentEffect !== lastEffect.next);
}
/** 执行创建的effect */
export function commitHookEffectListCreate(
flags: HookEffectTag,
lastEffect: Effect | null
) {
commitHookEffectList(flags, lastEffect, (effect) => {
const create = effect.create;
if (typeof create === "function") {
// 设置destory
effect.destory = create() as EffectCallback;
}
});
}
可以看到,其本质就是遍历pendingPassiveEffect.update,找到哪些包含HookHasEffect的Effect对象,执行其create函数,并且把返回值作为destory存入effect对象。
commitHookEffectListDestory,同样是遍历,找到包含HookHasEffect tag的Effect对象,执行其destory函数 如下:
/** 执行destory的effect */
export function commitHookEffectListDestory(
flags: HookEffectTag,
lastEffect: Effect | null
) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destory = effect.destory;
if (typeof destory === "function") {
destory();
}
});
}
最后在看卸载的情况,即commitHookEffectListUnmount 把unmount中所有的effect都执行一遍,并且去除其tag上的HookHasEffect标记(如果有的话)
/** 执行卸载的effect */
export function commitHookEffectListUnmount(
flags: HookEffectTag,
lastEffect: Effect | null
) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destory = effect.destory;
if (typeof destory === "function") {
destory();
}
effect.tags &= ~HookHasEffect;
});
}
这样就完成了一次更新的Effect的运行,最后清空pendingPassiveEffect即可
所以,Effect的执行一定是从下到上的,因为commit Mutation是在归的阶段执行,收集的Effect自然也是从下到上的!