Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

    • 开始上手
    • useState 和 useReducer
    • useEffect
      • 目录
      • 定义
      • mount 阶段的 useEffect
      • update 阶段的 useEffect
      • useEffect 如何避免在 mount 时执行?
      • useEffect 调度执行
      • useEffect 怎么模拟类组件 lifecycle Api?
      • 参看链接
    • useRef 原理
  • 总结

  • React源码漂流记

  • react
  • hooks原理
jonsam
2022-04-14
目录

useEffect

# 目录

  • 目录
  • 定义
  • mount 阶段的 useEffect
  • update 阶段的 useEffect
    • effect deps 是如何比较的?
  • useEffect 如何避免在 mount 时执行?
  • useEffect 调度执行
  • useEffect 怎么模拟类组件 lifecycle Api?
  • 参看链接

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

由这个可知:

  • 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

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

由这个函数可知:

  • 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

# 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

可以看到:

  • 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

# useEffect 调度执行

看到这里,effect 的创建和管理是清晰了。但是在什么时机会调度执行 effect 呢?我们知道,effect 会在 mount 和 update 时执行。

# useEffect 怎么模拟类组件 lifecycle Api?

# 参看链接

  • 官方文档 (opens new window)
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
useState 和 useReducer
useRef 原理

← useState 和 useReducer useRef 原理→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式