请解释 useEffect?与 useLayoutEffect 的区别?
2023年2月2日
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(可选)
setup function
setup function:setup function 包含如何连结外部系统的代码,如果需要清除逻辑,可以在 setup function 中回传一个清除 function。
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
执行时机
- 当元件被加入时 (mount),
useEffect
会被第一次执行。 - 当每次元件重新渲染时,如果 dependencies 的值有改变,先将旧的 props 和 state 执行 cleanup function,再带着新的 props 和 state 执行 setup function。
- cleanup function 的代码,会在元件生命周期结束 (unmount) 时,执行最后一次。
使用 useEffect
,画面重新渲染时都会闪烁要怎么解?
解法:尝试将 useEffect
换成使用 useLayoutEffect
。
以下这一段我们会讨论跟 useEffect
很相近的 Hook - useLayoutEffect
。
而标题已经点出来,useLayoutEffect
通常会拿来处理当使用 useEffect
但画面会出现闪烁的情境,详细原因下面会提到。
useLayoutEffect
是什么?
useLayoutEffect
其实是 useEffect
的一种版本,传入的参数也一样,只是执行时机不一样,它会在浏览器重绘 (repaints) 前执行。
useLayoutEffect
可能会造成性能的问题,因为在 useLayoutEffect
里的代码会阻碍浏览器重绘 (repaints) ,太频繁使用可能会造成整个应用程式缓慢。因此通常会建议先使用 useEffect
,如果不能解决问题,才会选择使用 useLayoutEffect
。
useEffect
和 useLayoutEffect
比较
以下提供一个代码范例,可以明显感到这两者差别。 (备注:以下代码范例是为了凸显这两者差别,实际上开发并不会这样写)
代码
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。