通俗易懂的React原理(四):浅谈useEffect

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0

上一篇我们深入了解了React的hooks的原理,包括它是以什么样的形式被React记录的,怎么样被执行的,并且还以useReducer/useState为例,学习了我们最常用到的一个hooks的执行逻辑,并且。

那么今天我们再来看看我们另一个也非常常用的hooks,useEffect

useEffect

mountEffect

有了上一篇的经验,我们知道了hooks的实现主要在mountxxx和updatexxx方法里,那么我们先看看useEffect的mount逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mountEffectImpl(
fiberFlags: Flags, // 传入的实参为PassiveEffect | PassiveStaticEffect
hookFlags: HookFlags, // 传入的实参为HookPassive
create: () => (() => void) | void,
createDeps: Array<mixed> | void | null,
// 以下参数用不到,只有开了enableUseEffectCRUDOverload这个flag才有用
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = createDeps === undefined ? null : createDeps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags,
createEffectInstance(),
create,
nextDeps,
);
}

其中,mountWorkInProgressHook上一篇我们已经看过了,就是把这个hook插入到Fiber.memoizedState这个链表尾部

而nextDeps就来自我们调用useEffect时传入的依赖数组

给currentlyRenderingFiber打上PassiveEffect | PassiveStaticEffect 这两个flag,这样在commit阶段,React就知道这个Fiber上有副作用需要执行。而没有相应flag的Fiber节点就会被跳过来作为优化。详细逻辑要等后面讲commit阶段的文章。

接下来,通过pushSimpleEffect生成一个effect对象,并且将这个effect挂到Fiber.updateQueue这个链表节点上,保存起来。你的函数组件里每有一个useEffect,那组件对应的Fiber的updateQueue链表里就多一个effect。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function pushSimpleEffect(
tag: HookFlags,
inst: EffectInstance,
create: () => (() => void) | void,
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): Effect {
const effect: Effect = {
tag,
create,
deps: createDeps,
inst,
// Circular
next: (null: any),
};
return pushEffectImpl(effect);
}

function pushEffectImpl(effect: Effect): Effect {
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
}
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}

updateEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;

// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// $FlowFixMe[incompatible-call] (@poteto)
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushSimpleEffect(
hookFlags,
inst,
create,
nextDeps,
);
return;
}
}
}

currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags,
inst,
create,
nextDeps,
);
}

可见这段逻辑依旧是很简单啊,中间有个判断依赖项是否发生变化的逻辑, 很简单的遍历数组比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
if (prevDeps === null) {
return false;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}

如果依赖项没发生变化,那么我们都知道这个effect是不会被执行的。所以我们看下区别,就是如果依赖项没发生变化,那么effect对象的hookFlags字段就不会包含HookHasEffect这个值,这会导致最终commit阶段不会执行这个effect。

最终执行effect

其实上面两个方法是调用useEffect的时候发生的事,但是我们都知道,盗用useEffect是在渲染过程中,执行函数组件函数体时候发生的事。但是里面的effect真正执行,是在渲染完成后。上面方法也可以看到,渲染过程中只是把effect挂到fiber上,并且打上对应的flag。

真的执行,在flushPassiveEffectsImpl方法里。

首先,需要递归执行上一次渲染的effect的return方法,在commitHookEffectListUnmount方法里,里面的destroy就是我们写的effect里面的return方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
try {
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 inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
inst.destroy = undefined;

safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}

然后,递归的执行组件的这一次渲染的effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export function commitHookEffectListMount(
flags: HookFlags,
finishedWork: Fiber,
) {
try {
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
let destroy;
const create = effect.create;
const inst = effect.inst;
// $FlowFixMe[incompatible-type] (@poteto)
// $FlowFixMe[not-a-function] (@poteto)
destroy = create();
// $FlowFixMe[incompatible-type] (@poteto)
inst.destroy = destroy;

}
effect = effect.next;
} while (effect !== firstEffect);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}

其中我们都可以注意到,有if ((effect.tag & flags) === flags)这个判断,flags的值就是HookPassive | HookHasEffect。所以如果依赖项没发生变化,那么effect.tag是不会包含HookHasEffect的,就会被跳过执行。所以这就实现了仅在依赖项发生变化时执行的功能

一些感悟

希望这篇文章能为大家解开useEffect的神秘面纱。虽然useEffect可能不如useReducer/useState那么常用,但是我打赌它一定是出问题最多的hooks。我刚学React的时候,也有过许多的疑惑,比如它的依赖项,比如它的执行时机等等。

就比如我之前就想过,它的依赖项一定要是useState返回的值吗?那么这篇文章看完之后其实我们也就有了答案,对于依赖项的值,没有任何约束。只要重新渲染了,他就会严格比较依赖项是否发生变化,不论依赖项是从哪里来的值。只不过前提是能触发重新渲染哦。所以这里也要纠正一个对useEffect的常见误区,用它来监听某个变量发生变化时执行操作。useEffect并不是在依赖项发生变化时执行,而是在重新渲染的时候才有可能执行。如果你的依赖项是一个自行定义的变量,或者useRef定义的变量,那么useEffect是起不到监听效果的呦


通俗易懂的React原理(四):浅谈useEffect
https://miku03090831.github.io/2025/08/02/通俗易懂的React原理(四):浅谈useEffect/
作者
qh_meng
发布于
2025年8月2日
许可协议