useEffect 基本使用
useEffect 根据传参个数和传参格式,它的执行次数和执行结果是不同的。
useEffect(setup, dependencies?)
- 在没有依赖项数组时,每次渲染之后都会执行 Effect
- 依赖项数组可以设置多个依赖项,其中任意一项发生变化,Effect 都会执行
需要注意的是:
当依赖项是引用类型时,React 会比较依赖项的内存地址是否一样,如果一致,Effect 不会执行。示例如下:
js
import React, { useState, useEffect } from "react";
const Child = ({ data }) => {
useEffect(() => {
console.log("useEffect");
}, [data]);
return <div>{data.x}</div>;
};
let data = { x: 0 };
const App = () => {
const [count, setCount] = useState(0);
console.log("render");
return (
<div>
<button
onClick={() => {
data.x = data.x + 1;
setCount(count + 1);
}}
>
click
</button>
<Child data={data} />
</div>
);
};
export default App;
点击 click 之后,对象 data 中的属性值会发生变化,但是传入组件 <Child /> 组件的内存地址没有变化,所以 console.log("useEffect") 不会执行。为解决这个问题,应该使用对象中的属性作为依赖,而不是整个对象。应该将上面示例中组件 <Child /> 修改如下:
js
const Child = ({ data }) => {
useEffect(() => {
console.log("useEffect");
}, [data.x]);
return <div>{data.x}</div>;
};
useEffect 的执行时机和 useLayoutEffect
useEffect在渲染完成后异步执行,不会阻塞浏览器的绘制操作。
然而并非所有的操作都适合放在 useEffect 中延迟执行。例如:在浏览器下一次绘制之前需要操作 DOM 改变页面样式,或者依赖布局信息渲染组件。如果放在 useEffect 中执行,会出现闪屏问题。而 useLayoutEffect 在浏览器执行绘制之前被同步执行,使用 useLayoutEffect 可以避免这个问题。
准确绑定依赖
js
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect");
const timer = setInterval(() => {
console.log("setInterval");
setCount(count + 1);
}, 1000)
return () => clearInterval(timer);
}, [])
return <p> {count} </p>;
}
在上面例子中,useEffect 用到的依赖项 count,却没有声明在依赖项中,useEffect 不会重复执行(只在组件挂载时打印了一次 useEffect),setInterval 中拿到的 count 始终为 0,它后面每一秒都会调用 setCount(0 + 1),得到的结果始终为 1。下面是两种可以正确解决依赖的方法:
1. 在依赖项数组中包含所有在 Effect 中用到的值
将 Effect 中用到的外部变量 count 添加到依赖项数组中:
js
useEffect(() => {
console.log("useEffect");
const timer = setInterval(() => {
setCount(count + 1);
}, 1000)
return () => {
console.log(`return${count}`);
clearInterval(timer);
}
}, [count])
可以看出依赖项数组是正确的,并且解决了上面的问题。但是也可以发现,随之带来的问题是:定时器会在每一次 count 改变后销毁和重新创建,这并不是我们想要的结果。
2. 第二种方法是修改 Effect 中的代码来减少依赖项
js
useEffect(() => {
console.log("useEffect");
const timer = setInterval(() => {
setCount((count) => count + 1);
}, 1000)
return () => {
console.log('return');
clearInterval(timer);
}
}, [])
修改 Effect 内部的代码使 useEffect 的依赖更少,这需要一些移除依赖常用的技巧。如:setCount 还有一种函数回调模式,这种模式不需要关心当前值是什么,只需要对 "旧的值" 进行修改即可。这样就不需要把 count 写进依赖项数组这种方式来告诉 React。
是否需要清除副作用
如果只是在 React 更新 DOM 之后运行一些额外的代码,如发送网络请求等,则无需清除操作。
需要清除的是那些执行之后还有后续的操作,如定时器或者页面的监听事件等。为防止内存泄漏,可以通过 useEffect 的 return 销毁通过 useEffect 注册的监听。
js
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect");
return () => {
console.log("return");
}
}, [count])
return (
<div>
<p> {count} </p>
{console.log("dom")}
<button onClick={() => setCount(count + 1)}>
click
</button>
</div>
)
}
从 dom、return、useEffect 的打印结果可以看出,useEffect 的清除函数在每次重新渲染时都会执行,而不是只在组件卸载时执行。清除函数是在新的渲染之后执行的。