作用
useEffect接收一个函数,可以让用户在函数组件中执行副作用操作,如:
- 设置订阅和事件处理
- ajax请求等异步操作
- 更改DOM对象及其他会对外部产生影响的操作等
使用方式
useEffect(create[, deps]);
第一个参数是要执行的 effect,而第二个参数是依赖项,依赖项是选填的。
例如
function App() {
useEffect(() => {
document.title = 'example'; // 副作用操作
});
return <div />;
}
执行时机
传递给useEffect的函数(effect)会在浏览器完成布局与绘制之后延迟(异步)执行,这里的异步实现优先级如下:setImmediate > MessageChannel > setTimeout,并且 React 会保证每次运行 effect 的时候 DOM 都已经更新完毕。虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行 ,这是官网上的一句描述,很不起眼的一句话,甚至不知道怎么理解这句话,我一开始也这样,直到后面看到这样一个例子:
import "./styles.css";
import { useState, useEffect } from "react";
export default function App() {
const [a, setA] = useState("a");
const [b, setB] = useState("b");
console.log("[render]", a, b);
useEffect(() => {
console.log("[useEffect]", a, b);
});
function handleClickWithPromise() {
Promise.resolve().then(() => {
console.log("async handler1", a, b);
setA("aa");
console.log("async handler2", a, b);
setB("bb");
console.log("async handler3", a, b);
});
}
function handleClick() {
console.log("sync handler1", a, b);
setA("aaa");
console.log("sync handler2", a, b);
setB("bbb");
console.log("sync handler3", a, b);
}
return (
<div className="App">
<button onClick={handleClickWithPromise}>
{a} - {b} with Promise
</button>
<button onClick={handleClick}>
{a} - {b} without Promise
</button>
</div>
);
}
这个例子不仅关乎到 useEffect 的执行时机,还涉及到 setState 的执行方式。简单的说 setState 的执行会触发组件的重新渲染,即函数的重新执行。setState 本身是同步执行的,但是在 由 React 控制的 事件处理函数,以及生命周期函数(类组件)调用 setState 时会将多个 setState 进行合并然后延迟执行,在 React控制之外的 如 setTimeout/setInterval、Promise等里面执行 setState 则不会合并处理,表现为同步执行。所以上述例子当我们点击 {a} - {b} with Promise
在 Promise 中调用 setState 时,会立即同步执行重渲染,再来看官网这句话,便明白为什么会看到这样的打印结果。
有条件的执行
默认情况下,effect 会在每轮组件渲染完成后执行,但有些时候我们不想要这样,可能只是想挂载完后设置订阅,或者某个数据改变后才执行effect,以此来做一些优化或者避免 bug 的发生。此时我们可以给 useEffect 传递第二个参数,它是 effect 所依赖的值的数组,当设置了第二个参数 deps 后,effect只会在所依赖的值发生变化时(使用 Object.is 进行比较)才运行。
需要清除的effect
有一些副作用是需要清除的,比如我们绑定的事件在组件卸载的时候需要解绑等,不然可能会导致一些意料之外的错误或者内存溢出,在类组件中通常会在 comonponentWillUnmount(vue则为beforeDestory )中清除副作用,而在useEffect中,我们可以使 effect 返回一个函数,在该函数中清除副作用,React将会在执行清除操作(组件卸载的时候)时调用该函数,我们称之为清理函数。例如:
function App() {
useEffect(() => {
const handleScroll = () => {};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
return <div />
}
- 首次渲染组件清理函数不会运行
- 清理函数的运行时间点是每次运行副作用函数之前
- 组件被销毁时一定会运行清理函数
在React v17.0之前 useEffect 的清理函数是同步运行的,在React v17.0中清理函数更新为异步运行 —— React v 17.0
看如下代码,首次进入和点击 increase 1
分别打印什么?顺序是怎么样的
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count is ${count} effect`);
return () => console.log(`clear count ${count} effect`);
}, [count]);
console.log(`count is ${count} render`);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>increate 1</button>
</div>
);
}
类比生命周期
如果你熟悉 React Class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 —— 使用 Effect Hook。如下面的类组件例子
class App extends React.Component {
componentDidMount() {
const { id } = this.props;
fetchData(id);
subsribe(id);
window.addEventListener(...);
...
}
componentDidUpdate() {
const { id } = this.props;
fetchData(id);
subscribe(id);
...
}
componentWillUnmount() {
removeSubscribe(this.props.id);
window.removeEventListener(...);
...
}
...
}
可以看到,我们在 componentDidMount 和 componentDidUpdate 中书写了相同的代码,这在我们日常开发中是非常常见的,因为我们希望在组件挂载和更新的时候做一些同样的操作,比如重新获取数据,亦或是在组件卸载的时候清除副作用,当我们有很多类似的操作的时候,不仅会书写很多重复的代码,而且相关联的代码分散在不同的生命周期函数中,当代码量多且复杂的时候就会变得不好管理。而改用 useEffect Hooks 的话会变成怎么样呢?
function App(props) {
const { id } = props;
useEffect(() => {
fetchData(id);
}, [id]);
useEffect(() => {
subscribe(id);
return () => removeSubscribe(id);
}, [id]);
useEffect(() => {
window.addEventListener(...);
return () => window.removeEventListener(...);
}, [...]);
...
}
基于 useEffect 的这种设计,我们不用再去考虑当前的 effect 是“挂载”还是“更新”,可以很好的实现 关注点分离 ,还可以在 effect 中返回一个函数,函数里面清除该 effect 中存在的副作用影响,相关代码都汇聚到了一块。当代码量和复杂度提高的时候甚至可以提取成自定义Hooks进行使用。
模拟componentDidMount
useEffect(() => {
console.log('模拟componentDidMount')
}, [])
模拟componentDidUpdate