前言
React 提供了 useEffect 钩子来处理与组件生命周期无关的副作用,例如数据获取、订阅事件、手动操作 DOM 等,其确保了副作用代码的正确执行。通过合理地使用 useEffect,并且在需要时进行清理,我们可以更好地管理组件的副作用,同时提高了代码的可读性和可维护性。那么到底什么是副作用呢?useEffect 又该如何使用呢?useEffect 的执行顺序是什么样的?在使用 useEffect 过程中有什么需要注意的事项呢?
📌 副作用(side effect)
副作用(side effect)是指 React 组件与组件之外的世界进行交互的行为,我们可以将副作用视为某些代码执行了一些操作并产生了附加的影响,例如从 API 请求数据、设置计时器、手动操作 DOM 等都属于副作用的范畴。当然,毫无疑问,React 需要副作用执行一些交互,而不仅仅是渲染逻辑。在 React 中,副作用不应该出现在渲染逻辑中,但可以在两个地方发生。首先是事件处理程序(Event Handlers),当某些操作触发了事件处理程序,例如 onClick 和 onSubmit 等,我们可以在这里执行副作用操作。然而,有时仅仅依靠事件处理程序是不够的,我们可能需要在组件渲染时执行一些操作。这时,我们可以利用 Effects(useEffect) 来生成副作用。通过 useEffect hook 允许我们在组件实例的不同生命周期执行副作用。
📌 useEffect
const [title, setTitle] = useState('default title');
useEffect(() => {
// 执行副作用
if (!title) return;
document.title = title;
return () => {
document.title = 'default title'; // 清除副作用
};
}, [title]); // 依赖数组
React 提供了 useEffect hook 来处理副作用,其代替了类式组件写法中组件的生命周期函数。useEffect 接受两个参数:一个是副作用函数,另一个是依赖数组。副作用函数定义了需要执行的操作,并且可以返回一个函数用以清理副作用,依赖数组用于指定副作用函数依赖的变量。useEffect 就像一个事件监听器,监听依赖数组中的变量是否发生变更,当依赖数组的变量发生变更时,副作用函数就会被执行。
依赖数组
const [number, setNumber] = useState(5);
const [duration, setDuration] = useState(0);
const mins = Math.floor(duration);
const secs = (duration - mins) * 60;
const formatDur = () => `${mins}:${secs < 10 ? "0" : ""}${secs}`;
useEffect(() => {
document.title = `${number} - ${mins}/${secs}: ${formatDur()}`;
}, [number, mins, secs, formatDur]);
什么样的内容应该被写到依赖数组中呢?首先,副作用函数中所涉及到的所有 state、props 和 context value 应该被包含在依赖数组中,除此之外,那些引用了 state、props 和 context value 的函数和变量也应该包含在内,例如上述例子中的 mins、secs 和 formatDur 都应该包含在依赖数组中。总结而言,所有的反应值(reactive values)都应该包含在依赖数组中,所谓反应值指的是 state、props、context value 和其他本身引用了反应值的内容。这么做的目的是为了避免出现过期闭包(stable closure)。不过如果你启用了 eslint 就不要担心,当你忘记添加依赖数组项时,eslint 会提示你的。不过,值得注意的是,不要将对象或数组作为依赖数组项,这会导致副作用函数会在每一次渲染时都执行,因为对象和数组在每次重新渲染时会被重新创建,即使内容一样,但引用不一样,React 会视为其发生了变化,从而会执行副作用函数。
依赖数组区别
依赖数组项发生变更时,副作用函数才会被运行,通过依赖数组项,React 就知道什么时候应该执行副作用,不同的依赖数组项会导致副作用函数在组件不同的生命周期阶段执行:
- 不设置依赖数组:副作用会在每一次渲染时都执行(render);
- 依赖数组为空数组:副作用仅会在组件挂载时执行(mount);
- 依赖数组有多项:副作用会在组件挂载(mount)和依赖改变时执行(re-render);
优化依赖数组
我们必须在依赖数组中包含所有反应值,如果依赖数组项过多可能会导致副作用运行过于频繁,这也可能会引起一些预料之外的问题,那么是否有一些优化的手段来使得一些依赖项没有必要呢,当然有!
- 移除函数依赖项:如果函数只在副作用中使用,那么可以将“函数移到副作用函数中”,而不是放在外层作为依赖项,例如上面例子中 formatDur 如果没有其他地方使用就可以放到副作用函数中;如果在许多地方被使用,那可以使用 useCallBack 来记忆化函数。
- 移除对象依赖项:不要将整个对象作为依赖项,而是将需要的属性值作为依赖项,当然这些属性值要是原始值,例如字符串、布尔值和数值等;如果必须要将对象作为依赖项,则考虑使用 useMemo 来记忆化对象,使得取值真正发生变化时才去执行副作用;
- 其他优化策略:如果依赖项有多个相关联的属性,可以考虑使用 reducer(useReducer);从 useState 中得到的 setState 方法和从 useReducer 中得到的 dispatch 方法不需要作为依赖项,React 已经保证了他们在多次渲染之间是稳定的。
清理函数(clean function)
依赖数组不同的设置使得副作用函数在组件不同的生命周期阶段执行,其主要涉及组件的挂载和重新渲染。既然 useEffect 是代替了类式组件写法中组件的生命周期函数,当我们卸载组件时,我们也希望执行一些代码,这个时候我们就可以借助从副作用中返回的一个所谓的清理函数来做到。清理函数主要是为了清理副作用,其会在两个时机执行:
- 在再次执行副作用之前被执行;
- 在组件实例被卸载之后被执行;
function MyDemo() {
console.log("render");
const [title, setTitle] = useState("default");
const handleClick = () => {
console.log("click");
setTitle("title" + Math.floor(Math.random() * 100));
};
useEffect(() => {
document.title = title;
console.log("useEffect");
// 返回清理函数
return () => {
console.log("clean");
document.title = "default";
};
}, [title]);
return (
<div>
<button onClick={handleClick}>click</button>
</div>
);
}
在上述例子中,当我们点击按钮时会更新 title,通过副作用会同步更新文档的标题,从打印的 console.log 内容可以看到,点击按钮更新标题后,触发了 re-render,然后在执行副作用前先一步执行了清理函数。
当然清理函数是可选的,如果不需要进行清理操作就不需要返回清理函数。那么什么时候我们需要清理函数呢?以下给出几个常见的应用场景:
- 副作用包括接口请求时,可以借助清理函数来取消请求,避免多次请求出现时序问题;
- 副作用包括 API subscription 时,可以借助清理函数取消;
- 副作用包括计时器时,可以借助清理函数取消计时器;
- 副作用添加了事件监听,可以借助清理函数来取消监听;
执行顺序
依赖数组项包含了反应值,依赖数组项发生变化时组件会发生 re-render,同时副作用会监听到变化会同步副作用函数,同时副作用返回了清理函数,我们有了方法可以在组件卸载时执行一些代码了,至此,useEffect 涵盖了组件的整个生命周期。那么这个执行顺序究竟是怎么样的呢?
- 首先,整个过程从组件实例挂载开始,也就是初始化渲染,然后 React 会将渲染结果 commit 给 DOM,浏览器再将结果绘制到屏幕上;只有浏览器将组件实例绘制到屏幕上后,副作用才会被执行,而不是在初始化渲染后就执行副作用,因此效果本质上是异步的,采用这种方式是副作用可能会包含一个长任务从而会阻塞浏览器内容绘制;
- 状态发生该变,组件会触发 re-render, React 会将重新渲染结果 commit 给 DOM,浏览器将结果绘制到屏幕上,不过在这之前会执行 layout effect(通过 useLayoutEffect,几乎很少使用),副作用监听到作为依赖项的状态发生改变会执行副作用,此外如清理函数一节中描述,在副作用执行前还会执行清理函数;
- 最后是组件卸载,卸载后会执行清理函数。
总结
综上所述,本文主要介绍了 useEffect 的相关知识点,包括依赖数组、清理函数、生命周期和执行顺序等。通过掌握这些概念,我们可以更好地利用 useEffect 处理副作用,提高 React 组件的效率和可靠性。在我们实际开发中,使用 useEffect 应该是最后手段,只有我们没有其他合适的解决方案时才使用,避免过度使用 useEffect,例如:
- 应该使用事件处理程序来响应用户事件,而不是使用 useEffect 来响应用户事件;
- 对于大型应用应该使用 React Query 等专业第三方库来处理数据请求,而不是在组件挂载时通过 useEffect 来请求数据;
- useEffect 被过度用于同步状态,即基于另一个状态设置状态,应通过派生状态或事件处理程序。
原文链接:https://juejin.cn/post/7347140662657581092 作者:植物系青年