React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook
这一篇, 我们走入 Effect 相关的原理, 涉及的 hook 有 useEffect, useLayoutEffect, useInsertionEffect, useImperativeHandle
总体文章的思路为先通过useEffect较详细了解Effect相关处理, 然后比较其他Hook。 如果对源码不是很感兴趣可以直接看总结
一、useEffect
mountEffect
还是老规矩, 先找初次挂载的入口mountEffect。 这里通过判断了Mode判断他走入哪个分支, 本质上都是调用了mountEffectImpl。 只不过fiber flags传入的多了MountPassiveDev。 不过我们的关注重点一般都在hook flags的Passive
function mountEffect(create, deps) {
if ( (currentlyRenderingFiber$1.mode & StrictEffectsMode) !== NoMode) {
return mountEffectImpl(MountPassiveDev | Passive | PassiveStatic, Passive$1, create, deps);
} else {
return mountEffectImpl(Passive | PassiveStatic, Passive$1, create, deps);
}
}
接着看他调用的 mountEffectImpl。 这里一共做了三步, 具体可看注释
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 这里对传入的deps做了处理
// 这里给当前的Fiber打上了flags标签, 表示有副作用, 在commit阶段会使用到
currentlyRenderingFiber.flags |= fiberFlags;
// 挂载上hook的链表
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined, // 可以看到这里暂时是不收集destroy的
nextDeps,
);
}
接着继续看pushEffect。 这里主要就是根据useEffect hook对象形成完整的Effect链表。 放在hook链表上以及updateQueue
function pushEffect(tag, create, destroy, deps) {
// 生成了对应的Effect对象, 最后是要挂上hook链表的
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
// Circular
next: null
};
// 拿到当前Fiber的updateQueue
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 这里就是生成链表的过程了, 主要是处理了updateQueue和effect链表, 具体效果可以看下面的图示
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
来个例子, 我们来看一下走完hook之后该函数Fiber变成啥样啦
export default function MyApp() {
const [count, setCount] = useState(1);
const [count1, setCount1] = useState(2);
useEffect(() => {
console.log('1');
return () => console.log('3')
}, [])
useEffect(() => {
console.log('2');
}, [count])
return (
<div onClick={() => setCount(count + 1)}>{count}</div>
)
}

updateEffect
了解完mountEffect做了什么后我们继续看updateEffect。 可以看到也是传入Passive
function updateEffect(create, deps) {
return updateEffectImpl(Passive, Passive$1, create, deps);
}
- 直接看
updateEffectImpl。 这里的流程也很清晰, 就不赘述了
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 拿到现在的依赖
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 这里比较了前后依赖, 如果一致的话处理hook之后就reuturn出去了
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 如果依赖不一致的话则给Fiber打上Passive的标志
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // effect的tag由hookFlags决定
create,
destroy,
nextDeps,
);
}
从mountEffect和udpateEffect都有一个关键的操作就是给当前的Fiber flags打上Passive的标志。 该标志在commit阶段的时候对Effect进行处理。
从这里你就能知道为什么
useEffect deps传入空数组的时候只会在初次渲染调用一次: 因为两个空数组中没得比较,故每次都认为l两个数组中的每一项都是一致的, 是没有变化的, 会直接return出去, 而不会打上flags标签useEffect为什么无论如何都最少会调用一次: 因为mountEffect的时候没有其他条件可以return出去, 一定会打上flags标签
如何处理useEffect(重点)
异步原理
先来一张宏观图(录制的是初次渲染阶段), 可以看到图的左边是进入了commit段,最后通过DOM API appendChild将处理好的DOM树挂载到页面上,然后交出线程, 浏览器进行渲染。 接着再执行了图的右边部分, 这一部分实际上就是在处理触发的useEffect的回调。
总的流程就是 commit阶段(宏任务) - 浏览器渲染 - useEffect相关回调执行(宏任务)

- 从链接点进去都有蛮详细的解释了, 这里就不多说了
- 以下具体的源码流程建议先看一篇commit的基础源码之后再看这个, 会舒服流畅很多, 因为都是同一个套路。 如果看不下去的话可以跳过源码直接看总结
那么我们继续看flushPassiveEffects这个函数究竟做了什么?
这里初始化了挺多东西, 我省略了,我们直接关注主流程。 可以看到主要就是调用了flushPassiveEffectsImpl
export function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
.....
try {
......
return flushPassiveEffectsImpl();
} finally {
......
}
}
return false;
}
- 那么
flushPassiveEffectsImpl又做了什么。 最重要的就是commitPassiveUnmountEffects和commitPassiveMountEffects。 接下去会重点介绍这两个函数
function flushPassiveEffectsImpl() {
.....
// 这里就是主流程了
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
....
flushSyncCallbacks();
return true;
}
处理destory
我称之为React常见四件套
该函数就是一个入口函数
export function commitPassiveUnmountEffects(firstChild: Fiber): void {
nextEffect = firstChild;
commitPassiveUnmountEffects_begin();
}
这里的
commitPassiveUnmountEffects_begin做了更多的事情, 为了整体性(而且涉及后面需要具体讲的逻辑), 故这部分放在后面讲
那么我们就只需要看两点
- 什么节点是目标节点
- 目标节点做了什么事情
对于第一点我们看xxcomplete, 作为commitPassiveUnmountOnFiber的入口函数, 这里限制了flags上有Passive, 可以回顾之前的mountEffect和updateEffect, 我们给节点打上的flags就有Passvive
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveUnmountOnFiber(fiber);
}
对于第二点, 我们回归关注commitPassiveUnmountOnFiber。 这里通过判断了组件函数, 走入commitHookEffectListUnmount
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
....
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
...
break;
}
}
}
这个函数就是最终真正处理Unmount的核心函数了。 可以看到这里就是拿到当前Fiber上的updateQueue, 然后从头开始, 进行遍历, 对于每一个effect, 拿出来effect的destroy,然后清空了, 再调用safelyCallDestroy(其实就是执行destroy函数, 之所以命名为safelyxx就是因为里面多了try catch去捕获错误)去处理
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) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
无奖竞猜: 初次执行调用flushPassiveEffects走到这里会执行destory吗
答案是不会的。 你可以回看mountEffect, 我们在挂载到hook链表上的时候, 此时存入的destroy为undefined
小总结
commitPassiveUnmountEffects函数主要流程就是找到Fiber树上带有Passive的节点, 取出它的destroy函数, 然后执行。 并且清空destroy函数
执行完commitPassiveUnmountEffects的下一步就是执行commitPassiveMountEffects, 我们继续看
处理create
commitPassiveMountEffects作为入口函数, 可以看到这里的流程也是经典四件套

我们还是关注两个点
- 什么节点是目标节点
- 目标节点做了什么事情
对于第一点, 看commitPassiveMountEffects_complete函数。 可以看到这里依旧是判断了flags上带有Passive
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveMountOnFiber(
root,
fiber,
committedLanes,
committedTransitions,
);
}
对于第二个点, 看commitPassiveMountOnFiber函数。 这里的逻辑还是相当多的, 对Fiber tag进行了判断, 其中对函数组件, 根组件等都有不同的处理。 这里关注函数组件。 可以看到调用了函数commitHookEffectListMount
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
...
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
break;
}
case HostRoot: ....
case LegacyHiddenComponent:
case OffscreenComponent:....
case CacheComponent: ....
break;
}
}
}
继续看commitHookEffectListMount。 可以看到这里的逻辑有点类似于commitHookEffectListUnmount。 也是去找到Fiber节点的updateQueue, 然后遍历他, 拿到每一个Effect上的create()函数, 这也就是我们传入给useEffect的回调函数, 拿到之后对他进行调用, 调用的结果再赋值给Effect的destory
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) {
// Mount
const create = effect.create;
effect.destroy = create();
}
.....
effect = effect.next;
} while (effect !== firstEffect);
}
}
小总结
这个函数的作用就是拿到Fiber上的updateQueue, 然后执行每一个Effect上的create()函数, 并且把执行结果赋值给Effect上的destory
到这里就能解答为什么我们在mountEffect的时候不直接给effect的destory进行赋值
如果一开始就直接赋值的话(而且你要赋值的话也得调用吧, 这调用时间也不合理hhh), 那么我们在初次处理useEffect的时候就会去调用destory函数了。 这显然不符合destory的定义
那我们对destory的定义是什么: 在每次依赖项变更重新渲染后,React 将首先使用旧值运行cleanup函数,然后使用新值运行setup函数
那怎么做到旧值执行destory, 新值执行create函数的呢

OK, 你以为走到这里就结束了? 别忘记我们上面跳过的commitPassiveUnmountEffects_begin。
处理卸载的destory
commitPassiveUnmountEffects_begin作为入口函数
这里主要是对删除的节点进行额外的处理。 为什么呢,要考虑到当我们一个组件要被卸载的时候, 其useEffect的cleanup函数应该都被执行一次。 这一块的逻辑目的就是这个
那我们根据上面的思路来说: 推测这里的思路应该就是找到被删除的Fiber节点, 然后拿到他的updateQueue, 如果updateQueue不为空的话, 就遍历每一个effect对象, 拿到destory函数进行执行
接下去看源码和我们推测的一不一样
function commitPassiveUnmountEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
const child = fiber.child;
// 子节点有删除节点, ChildDeletion就是当子节点被删除时会打上的flags
// 这一块就是挨个拿到删除的节点,然后调用commitPassiveUnmountEffectsInsideOfDeletedTree_begin
if ((nextEffect.flags & ChildDeletion) !== NoFlags) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const fiberToDelete = deletions[i];
nextEffect = fiberToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
fiberToDelete,
fiber,
);
}
// 断开Child链和Sibling链
const previousFiber = fiber.alternate;
if (previousFiber !== null) {
let detachedChild = previousFiber.child;
if (detachedChild !== null) {
previousFiber.child = null;
do {
const detachedSibling = detachedChild.sibling;
detachedChild.sibling = null;
detachedChild = detachedSibling;
} while (detachedChild !== null);
}
}
nextEffect = fiber;
}
}
// 这里删除了无关逻辑....
}
}
进来了commitPassiveUnmountEffectsInsideOfDeletedTree_begin 之后会顺着他的Child链和Sibling链(逻辑位于commitPassiveUnmountEffectsInsideOfDeletedTree_complete)调用commitPassiveUnmountInsideDeletedTreeOnFiber。 这个函数内部其实本质上就是调用了commitHookEffectListUnmount。commitHookEffectListUnmount就是处理destory的核心函数. 也就是说和我们推测的流程是一致的。
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
deletedSubtreeRoot: Fiber,
nearestMountedAncestor: Fiber | null,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
const child = fiber.child;
if (child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot,
);
}
}
}
总结
我们梳理一下useEffect的相关流程
首先是初次渲染阶段
render阶段的时候, 此时调用的是mountEffect, 会形成hook对象挂上hook链表。 并且形成Effect链表,udpateQueue存放对应的Effect链表。还会给Fiber打上Passive的flagcommit阶段的时候,在准备阶段利用MessageChannel, 让处理useEffect的流程异步执行, 也就是推到下一个宏任务处理- 异步处理
useEffect:- 首先先处理被删除的节点执行
destory函数(这里一般没有的) - 然后找到打上
Passive的Fiber节点(所有使用到useEffect的节点都会有该标签) - 然后会先处理
destory, 因为此时destory为undefined, 故不处理。 - 然后再处理
create, 此时是遍历updateQueue上存放的effect链表, 然后挨个执行create函数。 其执行结果再赋值给distory
- 首先先处理被删除的节点执行
(这也就是为什么useEffect在初次渲染的时候会执行一次)
接着函数可能因其他时候产生了变动, 重新执行组件函数, 此时走入了update流程
render阶段的时候, 此时调用的是updateEffect, 对前后的deps数组进行浅比较的判断, 如果前后依赖一致的话则不处理, 不一致的话则给当前的Fiber打上Passive的flagcommit阶段和初次渲染一致- 异步处理
useEffect:- 首先先处理被删除的节点执行
destory函数 - 然后找到打上
Passive的Fiber节点(这里只有依赖改变了的节点才会打) - 然后对于目标节点会先处理
destory, 此时因为初次渲染/上一次commit处理过, 所以如果代码中有的话这里一般都会有, 也是遍历updateQueue上的Effect链表, 拿到destory函数进行执行 create处理同初次渲染
- 首先先处理被删除的节点执行
二、useLayoutEffect
mountLayoutEffect
还是先看mountLayoutEffect。诶你会发现, 这里和mountEffect的逻辑也基本一致, 都是调用了mountEffectImpl, 最大的区别就是这里打上的hook flags是HookLayout
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
udpateLayoutEffect
再看udpateLayoutEffect, 也是直接调用的updateEffectImpl。 只不过他传入的flags还是HookLayout
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
既然是调用的一样的函数, 那么就说明他形成的hook对象, effect链都是一致的逻辑, 包括updateQueue。 体现不一样的就是每一个effect对象中的tag。 这个tag跟我们传入的flags有关
你会发现我们上面讲述的React如何处理useEffect的过程都是依赖着Passive的标签去寻找, 那么我们commit阶段处理useLayoutEffect的逻辑自然就不一样了
commit阶段是在mutation阶段去处理useLayoutEffect上的destory函数。 在layout阶段处理useLayoutEffect上的create函数
处理destory
对于destory函数, 其实本质上调用的还是commitPassiveUnmountOnFiber。 只不过和useEffect调用时机不一样
deps变更导致
对于deps产生变更需要执行的destory函数的执行路线是
commitMutationEffects(进入mutation阶段)->
commitMutationEffectsOnFiber(这里通过switch case tag进行筛选到FunctionComponent之后又通过筛选了fiber flags是否存在Update) ->
commitHookEffectListUnmount(这个在上面已经有讲解相关逻辑了, 注意这里传入的flags为Layout | HasEffect) ->
safelyCallDestroy(执行destory函数)

卸载导致
对于删除节点的话, 这里已经有详细讲解了。 调用逻辑就是
commitMutationEffects(进入mutation阶段)->
commitMutationEffectsOnFiber -> ( 这里无论Switch Case tag到哪个都一定会进入下一步, 上面deps的变更没有写是因为不重要hhh, 反正又会递归回来) ->
recursivelyTraverseMutationEffects (这个的第一步目标就是处理删除节点)->
commitDeletionEffects(删除节点流程的入口函数) ->
safelyCallDestroy
处理create
不同的就是传入的参数不同, useEffect调用的时候传入的flags为 HookPassive | HookHasEffect。 useLayoutEffect调用的时候传入的flags为 HookLayout | HookHasEffect。 commitHookEffectListMount内部就是通过传入的flags进行识别调用的
总结
useLayoutEffect和useEffect的处理逻辑基本都是一样的。 唯一不同的就是执行的时间。 基于flags的区别的做到一样的逻辑达到两种效果。
这里再总结一波加深印象
useLayoutEffect的destory函数执行时机在commit阶段的mutation阶段, create函数执行时间在commit阶段的layout阶段
useEffect的destory函数和create函数都是利用MessageChannel达到在渲染之后的宏任务进行。 同步顺序执行就是先destory后create

三、useInsertionEffect
mountInsertionEffect
可以看到也是调用了mountEffectImpl, 只不过传入的hook flags为HookInsertion
function mountInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
updateInsertionEffect
可以看到也是调用了updateEffectImpl,只不过传入的hook flags为HookInsertion
function updateInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
处理destory
其实我们在了解useLayoutEffect的时候已经接触了, 跟useLayout的调用时间是一样的
卸载导致
一样在commitDeletionEffectsOnFiber中,如图所示

deps变更导致
一样在commitMutationEffectsOnFiber中如图所示

处理create
hhh你看上面那个图, 其实已经处理了, 调用了commtHookEffectListUnmount处理destory之后就调用了commitHookEffectListMount去处理create
总结
可以看到, 其实上面三种hook都是依赖着flags在实现在不同时机去执行逻辑。
- 对于
useInsertionEffect来说flag为Insertion - 对于
useLayoutEffect来说flag为Layout - 对于
useEffect来说flag为Passive
我们把图再更新一下

四、useImperativeHandle
也许你会好奇, 诶我怎么把
useImperativeHandle放在这里讲, 这一篇不是在将effect相关的吗。 别急, 你往下看看
诶你会发现这不就类似于Effect的逻辑, 在依赖变更之后, 去执行一些操作。 让我们探究一下是不是这回事
Ok, 接下去按照我们的套路先看mountImperativeHandle
mountImperativeHandle
你会发现, 他依旧使用了mountEffectImpl。 且他的hook flags传入的是HookLayout。 故我们就了解了他的调用时机。 这里传入的create函数是imperativeHandleEffect。 然后将我们传入的ref, create作为参数传入
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// 这里对deps进行了处理, 将ref也放入deps中
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
imperativeHandleEffect又是什么呢。 可以看到基本逻辑是通过调用传入的create函数, 赋值给ref。 然后处理了destory函数, 在更新的时候能够重置ref值。
function imperativeHandleEffect<T>(
create: () => T,
// 传入的ref可以是ref对象也可以是一个函数
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create(); // 拿到暴露给父组件的ref对象
refCallback(inst); // 作为参数传入ref函数
return () => {
refCallback(null); // 这是destory函数,给他重置
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create(); // 拿到暴露给父组件的ref对象
refObject.current = inst; // 给传入的ref赋值
return () => {
refObject.current = null; // destory函数, 给他重置
};
}
}
updateImperativeHandle
这里的逻辑没什么好讲了, 跟mountImperativeHandle差不多
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,
);
}
故useImperativeHandle的一整个调用逻辑和useLayoutEffect都很类似。 区别就在于,useLayoutEffect的create函数和destory函数是我们传入的参数。 而useImperativeHandle的create函数和destory函数都是固定的
结束语
React hook的相关逻辑就到这一篇结束, 三篇下来我们一共了解了11个hook原理, 基本覆盖了常见的hook。 相信你看完这三篇也能够有一定收获, 对于什么场景使用什么hook也有了一定的了解。
转载自:https://juejin.cn/post/7304540179183370255