[

](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=566#/detail/pc?id=5807)

在 React 的面试中会对 Hooks API 进行一个区分度考察,重点往往会落在 useEffect 与 useLayoutEffect 上。很有意思,光从名字来看,它们就很相像,所以被点名的概率就很高。这一讲我们来重点讲解下这部分内容。

审题

在第 04 讲“类组件与函数组件有什么区别呢?”中讲过该类型的题目,其中提到描述区别,就是求同存异的过程。 那我们可以直接用同样的思路来思考下这道题。

先挖掘 useEffect 与 useLayoutEffect 的共性,它们被用于解决什么问题,其次发掘独特的个性、各自适用的场景、设计原理以及未来趋势等。

承题

根据以上的分析,该讲所讲解题目的答题思路就有了。

首先是论述共同点

  • 使用方式,也就是列举使用方式上有什么相似处,共同用于解决什么问题;
  • 运用效果,使用后的执行效果上有什么相似处。

其次是不同点

  • 使用场景,两者在使用场景的区分点在哪里,为什么可以或者不可以混用;
  • 独有能力,什么能力是其独有的,而另外一方没有的;
  • 设计原理,即从内部设计挖掘本质的原因;
  • 未来趋势,两者在未来的发展趋势上有什么区别,是否存在一方可能在使用频率上胜出或者淡出的情况。

17 | useEffect 与 useLayoutEffect 区别在哪里? - 图1

破题

共同点

使用方式

如果你读过 React Hooks 的官方文档,你可能会发现这么一段描述:useLayoutEffect 的函数签名与 useEffect 相同。

那什么是函数签名呢?函数签名就像我们在银行账号上签写的个人签名一样,独一无二,具有法律效应。以下面这段代码为例:

  1. MyObject.prototype.myFunction(value)

应用 MDN 的描述,在 JavaScript 中的签名通常包括这样几个部分:

  • 该函数是安装在一个名为 MyObject 的对象上;
  • 该函数安装在 MyObject 的原型上(因此它是一个实例方法,而不是一个静态方法 / 类方法);
  • 该函数的名称是 myFunction;
  • 该函数接收一个叫 value 的参数,且没有进一步定义。

那为什么说 useEffect 与 useLayoutEffect 函数签名相同呢?它们俩的名字完全不同啊。这是因为在源码中,它们调用的是同一个函数。下面的这段代码是 React useEffect 与 useLayoutEffect 在 ReactFiberHooks.js 源码中的样子。

  1. useEffect(
  2. create: () => (() => void) | void,
  3. deps: Array<mixed> | void | null,
  4. ): void {
  5. currentHookNameInDev = 'useEffect';
  6. mountHookTypesDev();
  7. checkDepsAreArrayDev(deps);
  8. return mountEffect(create, deps);
  9. },
  10. function mountEffect(
  11. create: () => (() => void) | void,
  12. deps: Array<mixed> | void | null,
  13. ): void {
  14. if (__DEV__) {
  15. if ('undefined' !== typeof jest) {
  16. warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
  17. }
  18. }
  19. return mountEffectImpl(
  20. UpdateEffect | PassiveEffect | PassiveStaticEffect,
  21. HookPassive,
  22. create,
  23. deps,
  24. );
  25. }
  26. useLayoutEffect(
  27. create: () => (() => void) | void,
  28. deps: Array<mixed> | void | null,
  29. ): void {
  30. currentHookNameInDev = 'useLayoutEffect';
  31. mountHookTypesDev();
  32. checkDepsAreArrayDev(deps);
  33. return mountLayoutEffect(create, deps);
  34. },
  35. function mountLayoutEffect(
  36. create: () => (() => void) | void,
  37. deps: Array<mixed> | void | null,
  38. ): void {
  39. return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
  40. }

可以看出:

  • useEffect 先调用 mountEffect,再调用 mountEffectImpl;
  • useLayoutEffect 会先调用 mountLayoutEffect,再调用 mountEffectImpl。

那么你会发现最终调用的都是同一个名为 mountEffectImpl 的函数,入参一致,返回值也一致,所以函数签名是相同的。

从代码角度而言,虽然是两个函数,但使用方式是完全一致的,甚至一定程度上可以相互替换。

运用效果

从运用效果上而言,useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。

虽然看起来很像,但在执行效果上仍然有些许差异。React 官方团队甚至直言,如果不能掌握 useLayoutEffect,不妨直接使用 useEffect。在使用 useEffect 时遇到了问题,再尝试使用 useLayoutEffect。

不同点

使用场景

虽然官方团队给出了一个看似友好的建议,但我们并不能将这样模糊的结果作为正式答案回复给面试官。所以两者的差异在哪里?我们不如用代码来说明。下面通过一个案例来讲解两者的区别。

先使用 useEffect 编写一个组件,在这个组件里面包含了两部分:

  • 组件展示内容,也就是 className 为 square 的部分会展示一个圆圈;
  • 在 useEffect 中操作修改 square 的样式,将它的样式重置到页面正中间。
  1. import React, { useEffect } from "react";
  2. import "./styles.css";
  3. export default () => {
  4. useEffect(() => {
  5. const greenSquare = document.querySelector(".square");
  6. greenSquare.style.transform = "translate(-50%, -50%)";
  7. greenSquare.style.left = "50%";
  8. greenSquare.style.top = "50%";
  9. });
  10. return (
  11. <div className="App">
  12. <div className="square" />
  13. </div>
  14. );
  15. };

下面再补充一下样式文件,其中 App 样式中规中矩设置间距,square 样式主要设置圈儿的颜色与大小,接下来就可以看看它的效果了。

  1. .App {
  2. text-align: center;
  3. margin: 0;
  4. padding: 0;
  5. }
  6. .square {
  7. width: 100px;
  8. height: 100px;
  9. position: absolute;
  10. top: 50px;
  11. left: 0;
  12. background: red;
  13. border-radius: 50%;
  14. }

然后执行代码, 你就会发现红圈在渲染后出现了肉眼可见的瞬移,一下飘到了中间。

17 | useEffect 与 useLayoutEffect 区别在哪里? - 图2

那如果使用 useLayoutEffect 又会怎么样呢?其他代码都不需要修改,只需要像下面这样,将 useEffect 替换为 useLayoutEffect:

  1. import React, { useLayoutEffect } from "react";
  2. import "./styles.css";
  3. export default () => {
  4. useLayoutEffect(() => {
  5. const greenSquare = document.querySelector(".square");
  6. greenSquare.style.transform = "translate(-50%, -50%)";
  7. greenSquare.style.left = "50%";
  8. greenSquare.style.top = "50%";
  9. });
  10. return (
  11. <div className="App">
  12. <div className="square" />
  13. </div>
  14. );
  15. };

接下来再看执行的效果,你会发现红圈是静止在页面中央,仿佛并没有使用代码强制调整样式的过程。

17 | useEffect 与 useLayoutEffect 区别在哪里? - 图3

虽然在实际的项目中,我们并不会这么粗暴地去调整组件样式,但这个案例足以说明两者的区别与使用场景。在 React 社区中最佳的实践是这样推荐的,大多数场景下可以直接使用useEffect,但是如果你的代码引起了页面闪烁,也就是引起了组件突然改变位置、颜色及其他效果等的情况下,就推荐使用useLayoutEffect来处理。那么总结起来就是如果有直接操作 DOM 样式或者引起 DOM 样式更新的场景更推荐使用 useLayoutEffect。

那既然内部都是调用同一个函数,为什么会有这样的区别呢?在探讨这个问题时就需要从 Hooks 的设计原理说起了。

设计原理

首先可以看下这个图:

17 | useEffect 与 useLayoutEffect 区别在哪里? - 图4

这个图表达了什么意思呢?首先所有的 Hooks,也就是 useState、useEffect、useLayoutEffect 等,都是导入到了 Dispatcher 对象中。在调用 Hook 时,会通过 Dispatcher 调用对应的 Hook 函数。所有的 Hooks 会按顺序存入对应 Fiber 的状态队列中,这样 React 就能知道当前的 Hook 属于哪个 Fiber,这里就是上一讲所提到的Hooks 链表。但 Effect Hooks 会有些不同,它涉及了一些额外的处理逻辑。每个 Fiber 的 Hooks 队列中保存了 effect 节点,而每个 effect 的类型都有可能不同,需要在合适的阶段去执行。

那么 LayoutEffect 与普通的 Effect 都是 effect,但标记并不一样,所以在调用时,就会有些许不同。回到前面的底层代码,你会发现只有第一个参数和第二个参数是不一样的,其中 UpdateEffect、PassiveEffect、PassiveStaticEffect 就是 Fiber 的标记;HookPassive 和 HookLayout 就是当前 Effect 的标记。如下代码所示:

  1. function mountEffect(
  2. create: () => (() => void) | void,
  3. deps: Array<mixed> | void | null,
  4. ): void {
  5. if (__DEV__) {
  6. if ('undefined' !== typeof jest) {
  7. warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
  8. }
  9. }
  10. return mountEffectImpl(
  11. UpdateEffect | PassiveEffect | PassiveStaticEffect,
  12. HookPassive,
  13. create,
  14. deps,
  15. );
  16. }
  17. function mountLayoutEffect(
  18. create: () => (() => void) | void,
  19. deps: Array<mixed> | void | null,
  20. ): void {
  21. return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
  22. }

标记为 HookLayout 的 effect 会在所有的 DOM 变更之后同步调用,所以可以使用它来读取 DOM 布局并同步触发重渲染。但既然是同步,就有一个问题,计算量较大的耗时任务必然会造成阻塞,所以这就需要根据实际情况酌情考虑了。如果非必要情况下,使用标准的 useEffect 可以避免阻塞。这段代码在 react/packages/react-reconciler/src/ReactFiberCommitWork.new.js 中,有兴趣的同学可以研读一下。

答题

那么以上就是本讲的全部知识点了,内容并不太多,重点主要在于分析思路。那么下面就可以进入答题环节了。

useEffect 与 useLayoutEffect 的区别在哪里?这个问题可以分为两部分来回答,共同点与不同点。

它们的共同点很简单,底层的函数签名是完全一致的,都是调用的 mountEffectImpl,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用。

那不同点就很大了,useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而 LayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect 做计算量较大的耗时任务从而造成阻塞。

在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。

17 | useEffect 与 useLayoutEffect 区别在哪里? - 图5

总结

本题仍然是一个讲区别的点,所以在整体思路上找相同与不同就可以。

这里还有一个很有意思的地方,React 的函数命名追求 “望文生义” 的效果,这里不是贬义,它在设计上就是希望你从名字猜出真实的作用。比如 componentDidMount、componentDidUpdate 等等虽然名字冗长,但容易理解。从 LayoutEffect 这样一个命名就能看出,它想解决的也就是页面布局的问题。

那么在实际的开发中,还有哪些你觉得不太容易理解的 Hooks?或者容易出错的 Hooks?不妨在留言区留言,我会与你一起交流讨论。

这一讲就到这了,在下一讲中,将主要介绍 React Hooks 的设计模式,到时见。