以下源码浅析的React版本为17.0.1,使用ReactDOM.render创建的同步应用,不含优先级相关。
数据结构
Effect类型保存相关信息
type Effect = {|
tag: HookFlags, // 标识是useEffect还是useLayoutEffect(HasEffect、Layout、Passive )
create: () => (() => void) | void, // 回调函数
destroy: (() => void) | void, // 销毁回调函数
deps: Array<mixed> | null, // 依赖数组
next: Effect, // 下一个Effect
|};函数组件的Effect信息保存在函数组件Fiber节点的updateQueue字段,updateQueue为一个单向环形链表。
type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};Fiber.updateQueue.lastEffect为最后一个Effect,lastEffect.next为第一个Effect。
这里值得注意的是Effect对象除了赋值给了updateQueue,同时也会赋值给Fiber节点中Hooks链表的对应Hook的memoizedState属性,用于后续的对比。
useEffect和useLayoutEffect
之前的文章里讲到了Mount时和Update时用的Hook不是同一个,但是useEffect和useLayoutEffect在Mount和Update时用的都是同一个方法,只是传入了不同的参数。
mountEffect和mountLayoutEffect
// Mount时useEffect
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect, // 赋值给Fiber.flags,与useLayoutEffect时不同的是多了个PassiveEffect
HookPassive, // 赋值给effect.tag
create, // 回调函数
deps, // 依赖数组
);
}
// Mount时的useLayoutEffect
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect,
HookLayout, // 与useEffect不同
create,
deps,
);
}mountEffect和mountLayoutEffect内部都是调用了mountEffectImpl,区别只是flags的标识不同,用于区分是useEffect还是useLayoutEffect。
updateEffect和updateLayoutEffect
// Update时useEffect
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
// Update时useLayoutEffect
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}和Mount时一样,Update时useEffect和useLayoutEffect内部使用的相同函数。
mountEffectImpl
mountEffectImpl函数里主要给Fiber节点添加了对应的flags,同时处理函数组件里Hooks链表,将Effect对象赋值给对应的workInProgressHook的memoizedState。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 处理hooks链表同时返回workInProgressHook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 为当前的Fiber节点添加flags
currentlyRenderingFiber.flags |= fiberFlags;
// pushEffect会返回Effect,同时赋值给workInProgressHook的memoizedState属性
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}内部调用了pushEffect函数来创建Effect对象。
pushEffect
pushEffect函数创建了Effect对象,并组装updateQueue的单向环形链表。
function pushEffect(tag, create, destroy, deps) {
// effect对象
const effect: Effect = {
tag, // effect的tag,用于区分useEffect和useLayoutEffect
create, // 回调函数
destroy, // 销毁函数
deps, // 依赖数组
// 环形链表
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
// fiber节点不存在updateQueue则需要初始化
if (componentUpdateQueue === null) {
// 创建新的updateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// 初始值
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 单向环形链表 lastEffect为最新的effect,lastEffect.next为第一个effect
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}当updateQueue不存在时,调用createFunctionComponentUpdateQueue创建新的updateQueue,否则就将新的effect添加到链表里。
createFunctionComponentUpdateQueue
createFunctionComponentUpdateQueue方法创建一个新的updateQueue。
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
};
}updateEffectImpl
updateEffectImpl函数内部多了一个判断传入的依赖数组是否相等的判断。
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 获取workInProgressHook,改变currentHook和workInProgressHook的指向
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
// 上一次的effect对象
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
// 上一次的依赖数组
const prevDeps = prevEffect.deps;
// 判读两个依赖数组是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(
hookFlags, // 对应的effect tag少了HookHasEffect,可视作无变化
create,
destroy, // 和mount时不同传入了destroy
nextDeps,
);
// 直接return
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}到现在分析了useEffect和useLayoutEffect在Mount和Update时如何创建Effect对象,与useState不同的时,它们不能通过调用dispatchAction来主动触发更新,而是随着useState变化触发更新的同时随着Fiber树的构建在commit阶段执行回调函数和销毁函数。
但是useEffect和useLayoutEffect的回调函数和销毁函数执行的时机是不同的,这也是它们之间的直接区别。
useEffect的异步执行
当workInProgressFiber树构建完成,进入commit阶段后,会异步调用useEffect的回调函数和销毁函数。
commit阶段内部又分为3个阶段
- Before Mutation阶段
- Mutation阶段
- Layout阶段
发起useEffect调度是在Before Mutation阶段执行的。
useEffect是异步调度的,需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的,所以在最终异步处理useEffect的时候已经收集好了。
发起useEffect调度
Before Mutation阶段会执行commitBeforeMutationEffects函数,这个函数同时也会执行类组件的getSnapshotBeforeUpdate生命周期。
function commitBeforeMutationEffects() {
// 省略无关代码...
while (nextEffect !== null) {
const flags = nextEffect.flags;
// 当flags包含Passive时表示有调用useEffect
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
// 将全局标识赋值为true,一个异步调度就会处理所有的useEffect,避免发起多个
rootDoesHavePassiveEffects = true;
// 通过调度器发起一个异步调度
scheduleCallback(NormalSchedulerPriority, () => {
// 处理useEffect
flushPassiveEffects();
return null;
});
}
}
// 遍历有副作用的Fiber节点
nextEffect = nextEffect.nextEffect;
}
}flushPassiveEffects函数内部会调用flushPassiveEffectsImpl函数,在这里会执行回调函数和销毁函数,因为是异步调度的,已经是渲染结束后了。
收集需要处理的useEffect
上面说到需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的。
Layout阶段会执行commitLayoutEffects函数,其中flags包含Update的Fiber节点的会执行commitLifeCycles函数。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 这里执行了useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 收集需要处理的useEffect
schedulePassiveEffects(finishedWork);
return;
}
// 省略无关代码...
}FunctionComponent会执行schedulePassiveEffects函数,schedulePassiveEffects函数中收集了需要执行回调函数和销毁函数的useEffect。
function schedulePassiveEffects(finishedWork: Fiber) {
// updateQueue环形链表同时存了useEffect和useLayoutEffect的Effect对象
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 遍历updateQueue
do {
const {next, tag} = effect;
if (
// HookPassive标识useEffect
(tag & HookPassive) !== NoHookEffect &&
// 当依赖数组没有发生变化时pushEffect的调用没有传入HookHasEffect,所以会被排除
(tag & HookHasEffect) !== NoHookEffect
) {
// 需要执行销毁函数
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
// 需要执行回调函数
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}需要执行销毁函数和回调函数的Effect对象分别存在两个数组中,数组的偶数下标为Effect对象,奇数下标为Fiber节点。
// 存需要执行回调函数
let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
// 存需要执行销毁函数
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];再看如何把Effect对象存入数组的。
// 需要执行回调函数
export function enqueuePendingPassiveHookEffectMount(
fiber: Fiber,
effect: HookEffect,
): void {
// 一次push两个不同数据,一个Effect对象,一个Fiber节点
pendingPassiveHookEffectsMount.push(effect, fiber);
// 省略代码...
}
// 需要执行销毁函数
export function enqueuePendingPassiveHookEffectUnmount(
fiber: Fiber,
effect: HookEffect,
): void {
pendingPassiveHookEffectsUnmount.push(effect, fiber);
// 省略代码...
}执行回调函数和销毁函数
这个过程由调度器异步调度执行,执行的函数为flushPassiveEffects的内部函数flushPassiveEffectsImpl。
function flushPassiveEffectsImpl() {
// 省略代码...
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
// 执行销毁函数destroy函数
// 偶数下标为HookEffect,奇数下标为fiber节点
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}
// 执行回调函数create函数
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
try {
const create = effect.create;
effect.destroy = create();
} catch (error) {
invariant(fiber !== null, 'Should be working on an effect.');
captureCommitPhaseError(fiber, error);
}
}
// 省略代码...
}useLayoutEffect的同步执行
和useEffect不同,useLayoutEffect就完全是同步的了,并且不需要像useEffect一样去收集Effect对象,而是直接通过updateQueue执行。
useLayoutEffect的回调函数执行在Layout阶段,销毁函数执行在Mutation阶段。
执行回调函数
上面说到Layout阶段会执行commitLifeCycles函数。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 收集需要处理的useEffect
schedulePassiveEffects(finishedWork);
return;
}
// 省略无关代码...
}在commitLifeCycles函数里调用了commitHookEffectListMount函数执行useLayoutEffect的回调。
commitHookEffectListMount
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
// 取出Fiber节点的updateQueue
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 遍历执行回调函数create函数
do {
// (tag => HookLayout | HookHasEffect) 标识effect对象为useLayoutEffect
if ((effect.tag & tag) === tag) {
// 执行回调函数
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}执行销毁函数
销毁函数的执行在Mutation阶段,Mutation阶段会执行commitMutationEffects函数,函数内部会对flags包含Update的Fiber节点再执行commitWork函数。
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的销毁函数
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
return;
}
}
}commitHookEffectListUnmount
commitHookEffectListUnmount函数和commitHookEffectListMount函数逻辑那还就是一样。
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
// 取出Fiber节点的updateQueue
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 & tag) === tag) {
// 执行销毁函数
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}至此useLayoutEffect执行完毕。
useImperativeHandle
useImperativeHandle相当于是一个useLayoutEffect的语法糖。
Mount
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return mountEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}Update
function updateImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}内部使用的依然是mountEffectImpl方法和updateEffectImpl方法,唯一不同的是create函数传入的是经过处理的imperativeHandleEffect。
imperativeHandleEffect
imperativeHandleEffect方法即是一个create方法,同时返回destroy函数。
function imperativeHandleEffect<T>(
create: () => T,
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}函数内部会通过判断ref为对象还是回调函数,分别执行不同的逻辑,返回不同的destroy函数。知道了内部的原理,其实可以很容易的用useLayoutEffect复现。
function useFakeImperativeHandle(ref, create, deps) {
useLayoutEffect(() => {
const inst = create();
if (typeof ref === "function") {
ref(inst);
return () => {
ref(null);
};
} else if (ref) {
ref.current = inst;
return () => {
ref.current = null;
};
}
}, deps);
}总结
useEffect和useLayoutEffect的函数本身在Mount和Update时调用的都是相同的函数,仅参数不同,最大的区别在于useEffect是异步执行,useLayoutEffect是同步执行。
useEffect和useLayoutEffect所使用的Effect对象储存在函数组件的Fiber节点的updateQueue中,它是一个单向环形链表,updateQueue.lastEffect为最新的Effect对象,lastEffect.next为第一个Effect对象,同时为了维护函数组件的Hooks链表,Effect对象也同时被添加到了Fiber节点的memorizedState属性中。
Effect对象通过tag字段区分是useEffect还是useLayoutEffect,HookPassive为useEffect,HookLayout为useLayoutEffect,HookHasEffect标记Effect的回调和销毁函数需要执行。
在Fiber树的render阶段通过renderWithHooKS方法执行函数组件,同时会执行内部的Hook,函数组件执行完成后创建了储存Effect对象的updateQueue链表。
在commit阶段,useEffect会在Before Mutation阶段通过commitBeforeMutationEffects函数发起异步调度,在Layout阶段通过函数commitLayoutEffects将需要执行回调函数和销毁函数的Effect分别收集到pendingPassiveHookEffectsMount和pendingPassiveHookEffectsUnmount数组。在commit阶段完毕后会经过调度器执行回调函数和销毁函数。
useLayoutEffect是同步执行的,它的销毁函数在Mutation阶段通过commitMutationEffects函数最终调用commitHookEffectListUnmount函数执行。它的回调函数会在 Layout阶段通过commitLayoutEffects函数最终调用commitHookEffectListMount函数执行。
如有错误,还望交流指正。