useEffect
# 目录
useEffect 在 hooks 中通常被充当生命周期使用,相比于类组件的 lifecycle Api,useEffect 的使用更加简洁精巧。其主要作用是对响应式的依赖项产生副作用。 在 useEffect 中通常是执行副作用的操作,包括数据更新、网络请求、数据存储等操作。
# 定义
在 react 包 ReactHooks.js 文件中,有 useEffect 的定义。
export function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
由这个可知:
- useEffect 同样是由 dispatcher 来管理的。
- create 是一个函数,这个函数可以返回一个函数,返回的这个函数是一个 cleaner。
- inputs 是依赖列表数组,依赖项必须是响应式的变量,如 props、state 或者依赖于二者的计算量。
# mount 阶段的 useEffect
useEffect 在 HooksDispatcherOnMount 中引用的是 mountEffect 函数,内部调用 mountEffectImpl 函数。
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
sideEffectTag |= fiberEffectTag;
// pushEffect 返回当前生成的 effect,这个 effect 被挂载到 hook.memoizedState 上。
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
pushEffect 函数将更新 effect 队列,将新的 effect 加入到队首。
function pushEffect(tag, create, destroy, deps) {
// 创建一个 effect 对象,由于是 mount 阶段 next为 null.
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
if (componentUpdateQueue === null) {
// 如果更新队列为空,则创建更新队列,这个队列里只记载了 lastEffect。
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 记录当前的 effect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 将 firstEffect 指向 effect,effect 指向 firstEffect。即时将 effect 放到更新队列的队首。
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return 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
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
由这个函数可知:
- effect 对象是由 componentUpdateQueue 来管理的,其内部是一个链表。lastEffect 指向链首,链首永远指向新加入的 effect。
- Effect 的管理和调度执行是分离的,因为 effect 都需要在一定的渲染时机才能触发。
- pushEffect 执行就会产生一个 effect,在 mount 阶段 pushEffect 必回执行一次,这说明 useEffect 在 mount 时一定会触发一次更新。
# update 阶段的 useEffect
update 阶段 useEffect 引用的是 updateEffect 函数,内部有 updateEffectImpl 实现。我们来看看这个函数:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
// 取出上一次的 effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 这里如果 prevDeps 不为空,则 nextDeps 一定不为空,因此如果为空,就不用产生 Effect 了。
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较 effect 是否发生了变化,只有 effect 变化,才生成 Effect,否则 tag 为 NoHookEffect
// tag 标记为 NoHookEffect 的 effect 不会被执行
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
// 依赖项发生了变化时,生成 effect
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# effect deps 是如何比较的?
areHookInputsEqual
函数比较依赖项是否发生了改变,这里我们来看下他的实现:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
function is(x: any, y: any) {
// 考虑两者都是 null 的情况
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看到:
- areHookInputsEqual 比较依赖项只是浅比较,并没有做深比较。
- 对于 Object 依赖而言,由于 useEffect 的依赖项通常是 state,而 useState 内部是替换旧状态的机制,这时也能够触发 effect。但是使用依赖项时要格外注意此类问题。
# useEffect 如何避免在 mount 时执行?
我们传入的 useEffect 的函数是会被当做 effect 来触发的,因此想要避免 effect 在某些时机的执行,我们可以如官网的推荐使用条件 effect,就是在执行 effect 加入一些条件,来避免一些不需要的执行。 想要避免 useEffect 在 mount 时执行,我们也可以使用这种方式做到。
const didMount = useRef<boolean>(false);
useEffect(() => didMount.current = true, []);
useEffect(() => {
if(didMount.current) {
// Only run after mounted.
}
}, [deps]);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# useEffect 调度执行
看到这里,effect 的创建和管理是清晰了。但是在什么时机会调度执行 effect 呢?我们知道,effect 会在 mount 和 update 时执行。
# useEffect 怎么模拟类组件 lifecycle Api?
# 参看链接
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56