请解释 useEffect?与 useLayoutEffect 的区别?

2023年2月2日

💎 加入 E+ 成長計畫 與超過 350+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

useEffect 是 React 中常用的一个 Hook,也是前端面试中经常被问到的 React 面试题,包括如何使用 useEffect、useEffect 的执行时机等。另一个与 useEffect 相似的 Hook 是 useLayoutEffect,这两者的比较也是 React 面试的高频题。

useEffect 是什么?

useEffect 是一个用于连接外部系统的 React Hook。 React 的函式元件需要是纯函式,但如果我们需要执行具有副作用(side effect) 的操作,例如:请求 API、使用第三方函式库,我们就需要将这些代码放在 useEffect 中执行。外部系统例如是:服务器端、浏览器提供的 API 或是第三方函式库。因为这部分不是由 React 本身处理的,所以称为外部系统。

useEffect 使用方法规则

只能在顶部呼叫

useEffect 也是一种 hook,因此只能在顶层呼叫,不能在回圈、条件式或者巢状的 function 中使用,想了解细节的朋友,可以前往这篇文章《为什么只能在最顶端层呼叫 Hook?从 useState 实作原理来回答》

useEffect 接受两个参数:setup function 和 dependencies(可选)

  1. setup function

    setup function:setup function 包含如何连结外部系统的代码,如果需要清除逻辑,可以在 setup function 中回传一个清除 function。

  2. dependencies dependencies 参数是可选的数组,可以传入 props、state 或元件中任何使用的变数。 React 会使用Object.is 算法来进行比较, (想了解Object.is 的细节可以阅读此篇文章《在 JavaScript 当中,==、=== 与Object.is ()的区别》)。如果 dependencies 中任意一个值与前一次不同,则此 useEffect 会重新执行。 。

import { useEffect } from "react";
import { createConnection } from "./blog.js";

function Article({ articleId }) {
  const [serverUrl, setServerUrl] = useState("https://blog.com/0");

  useEffect(() => {
    const connection = createConnection(serverUrl, articleId);
    connection.connect();

    // 回传 cleanip function
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, articleId]);
  // ...
}

useEffect 执行时机

  1. 当元件被加入时 (mount),useEffect 会被第一次执行。
  2. 当每次元件重新渲染时,如果 dependencies 的值有改变,先将旧的 props 和 state 执行 cleanup function,再带着新的 props 和 state 执行 setup function。
  3. cleanup function 的代码,会在元件生命周期结束 (unmount) 时,执行最后一次。

使用 useEffect,画面重新渲染时都会闪烁要怎么解?

解法:尝试将 useEffect 换成使用 useLayoutEffect

以下这一段我们会讨论跟 useEffect 很相近的 Hook - useLayoutEffect

而标题已经点出来,useLayoutEffect 通常会拿来处理当使用 useEffect 但画面会出现闪烁的情境,详细原因下面会提到。

useLayoutEffect 是什么?

useLayoutEffect 其实是 useEffect 的一种版本,传入的参数也一样,只是执行时机不一样,它会在浏览器重绘 (repaints) 前执行。

useLayoutEffect 可能会造成性能的问题,因为在 useLayoutEffect 里的代码会阻碍浏览器重绘 (repaints) ,太频繁使用可能会造成整个应用程式缓慢。因此通常会建议先使用 useEffect,如果不能解决问题,才会选择使用 useLayoutEffect

useEffectuseLayoutEffect 比较

以下提供一个代码范例,可以明显感到这两者差别。 (备注:以下代码范例是为了凸显这两者差别,实际上开发并不会这样写)

代码

import { useEffect, useLayoutEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count === 0) {
      const randomNum = 1 + Math.random() * 1000;
      setCount(randomNum);
    }
  }, [count]);

  return <div onClick={() => setCount(0)}>{count}</div>;
}

当我们执行上方代码时,连续点击 div 区块,会看到画面产生闪烁。

原因是,当你每次点击 div,此时 count 会被更新为 0,画面会重新渲染变为 0,同时,因为 count 被更新,也会触发 useEffect 执行。所以在重绘完成之后, useEffect 执行并把 count 更新为另一串随机数字,画面也会再渲染一次,因为两次渲染时间很快,所以造成闪烁。

useLayoutEffect 的差异是什么 ?

假设我们把上方代码的useEffect 换成useLayoutEffect,当你每次点击 div,此时 count 会被更新为 0,但这时,画面不会被重新渲染变为 0,而是先等待 useLayoutEffect 内的代码执行完毕之后,state 已经更新为新的随机数字,这时画面才进行重绘。

延伸问题

如果不指定 dependencies ,useEffect 什么时候会执行?

此 effect 会是在每次重新渲染元件后重新执行

如果 dependencies 中有 object 会怎么样?

下方代码的 options 物件会在元件每次重新渲染时,都是不同的物件,所以这个 useEffect 可能会在每次渲染时都重新执行,因为在 dependencies 中的 options 有改变。

function ChatRoom({ articleId }) {
  const [article, setArticle] = useState(null);

	// options 会在每次渲染时重新创建
  const options = {
    serverUrl: 'https://localhost:1234',
    articleId: articleId
  };

  useEffect(() => {
    const data = getArticle(options);
    setArticle(data)

	// options 每次渲染时值都不同,因此触发 useEffect 执行
  }, [options]);

如果要避免上方代码造成不必要触发 useEffect,其实可以把动态的物件放在 Effect 中,并把 dependencies 中的物件改为 string 或 number,如下。

function ChatRoom({ articleId }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
	  const options = {
	    serverUrl: 'https://localhost:1234',
	    articleId: articleId
	  };

    const data = getArticle(options);
    setArticle(data)

  }, [articleId]);

什么时候使用 useLayoutEffect

如上方说到,useLayoutEffect 频繁使用会损害应用程式的效能,只有在一些特别情况才建议使用。以下提供一个使用情境 (此例子来自于 React Docs Beta)。

假设今天我们要设计一个工具提示元件(tooltip),它会依照不同条件出现在某元素的上方、下方或旁边,这种设计条件代表说,我们会需要知道此元素准确的高度位置,才能判断要将 tooltip 显示在哪里。

React 的渲染步骤可以拆分为下

  • 在任何地方渲染 Tooltip (即使位置不正确)
  • 测量元素的高度,并决定放置 Tooltip 的位置
  • 重新渲染画面,这时 Tooltip 的位置才会是正确的

如果是使用useEffect 的话,Tooltip 的位置,可能是从 0 变到 10 的位置,这会造成画面闪烁、使用者体验不佳,如果是使用useLayoutEffect,React 则会在重绘前,就重新计算正确的位置,才渲染画面。

代码可参考此连结 React Docs Beta

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們