前面的文章介绍了 hooks和commit流程,算是前置知识,这篇来讨论一下useEffect的原理。
useEffect用来处理副作用,比如网络请求,dom操作等等, 其本质也是个hooks,包含hooks的memorizedState, updateQueue, next
Effect对象
useEfffect这个hooks的memorizedState存储的是Effect对象,其ts定义为
/** 定义effect */
export interface Effect {tags: HookEffectTag;// 依赖数组deps: HookDeps;// 传入的创建effectcreate: EffectCallback | null;// 清除effectdestory: 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 -...*/// 获取到hookconst hook = mountWorkInProgressHook();// 给fiber设置PassiveEffect 表示存在被动副作用(currentRenderingFiber as FiberNode).flags |= PassiveEffect;hook.memorizedState = pushEffect(// 初始化状态下,所有的useEffect都执行,所以这里flag设置为 Passive|HookHasEffectPassive | 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)) {// 创建一个FCUpdateQueueconst 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 {// 获取当前hookconst 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的destorydestory,deps);} else {/** 不等 表示hook有Effect */hook.memorizedState = pushEffect(Passive | HookHasEffect, // 注意这里是 Passive 是Effect的tag 区分fiber的tag PassiveEffectcreate,// 前一个副作用hook的destorydestory,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 {// 更新的effectupdate: Effect[];// 卸载的effectunmount: 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) {// 收集effectroot.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都执行destorpendingPassiveEffect.unmount.forEach((unmountEffect) => {commitHookEffectListUnmount(Passive, unmountEffect);});pendingPassiveEffect.unmount = [];// 处理update 的destory flag为Passive|HookHasEffectpendingPassiveEffect.update.forEach((updateEffect) => {commitHookEffectListDestory(Passive | HookHasEffect, updateEffect);});// 处理update的create flag为Passive| HookHasEffectpendingPassiveEffect.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必须完全相等 执行callbackcallback(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") {// 设置destoryeffect.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自然也是从下到上的!