【React Hooks 专题】useEffect 使用指南
共 7526字,需浏览 16分钟
·
2021-07-20 17:31
引言
Hooks 是 React 16.8 的新增特性,至今经历两年的时间,它可以让你在不编写 class 组件的情况下使用 state 以及其他 React 特性。useEffect
是基础 Hooks 之一,我在项目中使用较为频繁,但总有些疑惑 ,比如:
如何正确使用 useEffect
?useEffect
的执行时机 ?useEffect
和生命周期的区别 ?
本文主要从以上几个方面分析 useEffect
,以及与另外一个看起来和 useEffect
很像的 Hook useLayoutEffect
的使用和它们之间的区别。
useEffect 简介
首先介绍两个概念,纯函数和副作用函数。纯函数( Pure Function ):对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,这样的函数被称为纯函数。副作用函数( Side effect Function ):如果一个函数在运行的过程中,除了返回函数值,还对主调用函数产生附加的影响,这样的函数被称为副作用函数。useEffect
就是在 React 更新 DOM 之后运行一些额外的代码,也就是执行副作用操作,比如请求数据,设置订阅以及手动更改 React 组件中的 DOM 等。
正确使用 useEffect
基本使用方法:useEffect(effect)
根据传参个数和传参类型,useEffect(effect)
的执行次数和执行结果是不同的,下面一一介绍。
默认情况下, effect
会在每次渲染之后执行。示例如下:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
也可以通过设置第二个参数,依赖项组成的数组 useEffect(effect,[])
,让它在数组中的值发生变化的时候执行,数组中可以设置多个依赖项,其中的任意一项发生变化,effect
都会重新执行。示例如下:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
需要注意的是:当依赖项是引用类型时,React 会对比当前渲染下的依赖项和上次渲染下的依赖项的内存地址是否一致,如果一致,effect
不会执行,只有当对比结果不一致时,effect
才会执行。示例如下:
function Child(props) {
useEffect(() => {
console.log("useEffect");
}, [props.data]);
return <div>{props.data.x}</div>;
}
let b = { x: 1 };
function Parent() {
const [count, setCount] = useState(0);
console.log("render");
return (
<div>
<button
onClick={() => {
b.x = b.x + 1;
setCount(count + 1);
}}
>
Click me
</button>
<Child data={b} />
</div>
);
}
结果如下:
上面实例中,组件 <Child/>
中的 useEffect
函数中的依赖项是一个对象,当点击按钮对象中的值发生变化,但是传入 <Child/>
组件的内存地址没有变化,所以 console.log("useEffect")
不会执行,useEffect 不会被打印。为了解决这个问题,我们可以使用对象中的属性作为依赖,而不是整个对象。把上面示例中组件 <Child/>
修改如下:
function Child(props) {
useEffect(() => {
console.log("useEffect");
}, [props.data.x]);
return <div>{props.data.x}</div>;
}
修改后结果如下:
可见 useEffect
函数中的 console.log("useEffect")
被执行,打印出 useEffect。
当依赖项是一个空数组 [] 时 , effect
只在第一次渲染的时候执行。
useEffect 的执行时机
默认情况下,effect
在第一次渲染之后和每次更新之后都会执行,也可以是只有某些值发生变化之后执行,重点在于是每轮渲染结束后延迟调用( 异步执行 ),这是 useEffect
的好处,保证执行 effect
的时候,DOM 都已经更新完毕,不会阻碍 DOM 渲染,造成视觉阻塞。
useEffect 和 useLayoutEffect 的区别
useLayoutEffect
的使用方法和 useEffect
相同,区别是他们的执行时机。
如上面所说,effect
的内容是会在渲染 DOM 之后执行,然而并非所有的操作都能被放在 effect
都延迟执行的,例如,在浏览器执行下一次绘制前,需要操作 DOM 改变页面样式,如果放在 useEffect
中执行,会出现闪屏问题。而 useLayoutEffect
是在浏览器执行绘制之前被同步执行,放在 useLayoutEffect
中就会避免这个问题。
这篇文章中可以清楚的看到上述例子的具体实现:useEffect 和 useLayoutEffect 的区别
对比 useEffect 和生命周期
如果你熟悉生命周期函数,你可能会用生命周期的思路去类比思考 useEffect
的执行过程,但其实并不建议这么做,因为 useEffect
的心智模型和 componentDidMount
等其他生命周期是不同的。
Function 组件中不存在生命周期,React 会根据我们当前的 props 和 state 同步 DOM ,每次渲染都会被固化,包括 state、props、side effects 以及写在 Function 组件中的所有函数。
另外,大多数 useEffect
函数不需要同步执行,不会像 componentDidMount
或 componentDidUpdate
那样阻塞浏览器更新屏幕。
所以 useEffect
可以被看作是每一次渲染之后的一个独立的函数 ,可以接收 props 和 state ,并且接收的 props 和 state 是当次 render 的数据,是独立的 。相对于生命周期 componentDidMount
中的 this.state
始终指向最新数据, useEffect
中不一定是最新的数据,更像是渲染结果的一部分 —— 每个 useEffect
属于一次特定的渲染。对比示例如下:
在 Function 组件中使用 useEffect
代码示例 (点击在线测试):
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
结果如下:
在 Class 组件中的使用生命周期,代码示例:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
结果如下:
但是每次渲染之后都去执行 effect
并不高效。所以怎么解决呢 ?这就需要我们告诉 React 对比依赖来决定是否执行 effect
。
如何准确绑定依赖
在 effect
中用到了哪些外部变量,都需要如实告诉 React ,那如果没有正确设置依赖项会怎么样呢 ?示例如下 :
上面例子中, useEffect
中用到的依赖项 count
,却没有声明在卸载依赖项数组中,useEffect
不会再重新运行(只打印了一次 useEffect ), effect
中 setInterVal
拿的 count
始终是初始化的 0 ,它后面每一秒都会调用 setCount(0 + 1)
,得到的结果始终是 1 。下面有两种可以正确解决依赖的方法:
1.在依赖项数组中包含所有在 effect
中用到的值
将 effect
中用到的外部变量 count
如实添加到依赖项数组中,结果如下:
可以看到依赖项数组是正确的,并且解决了上面的问题,但是也可以发现,随之带来的问题是:定时器会在每一次 count
改变后清除和重新设定,重复创建/销毁,这不是我们想要的结果。
2.第二种方法是修改 effect 中的代码来减少依赖项
即修改 effect
内部的代码让 useEffect
使得依赖更少,需要一些移除依赖常用的技巧,如:setCount
还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可,这样就不需要通过把 count
写到依赖项数组这种方式来告诉 React 了,因为 React 已经知道了。
是否需要清除副作用
若只是在 React 更新 DOM 之后运行一些额外的代码,比如发送网络请求,手动变更 DOM,记录日志,无需清除操作,因为执行之后就可以被忽略。
需要清除的是指那些执行之后还有后续的操作,比如说监听鼠标的点击事件,为防止内存泄漏清除函数将在组件卸载之前调用,可以通过 useEffect
的返回值销毁通过 useEffect
注册的监听。
清除函数执行时机是在新的渲染之后进行的,示例如下(点击在线测试):
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect");
return () => {
console.log("return");
};
}, [count]);
return (
<div>
<p>You Click {count} times </p>
{console.log("dom")}
<button
onClick={() => {
setCount(count + 1);
}}
>
Click me
</button>
</div>
);
};
结果如下:
需要注意的是:useEffect
的清除函数在每次重新渲染时都会执行,而不是只在卸载组件的时候执行 。
参考文档
React Core Team 成员、Readux 作者 Dan 对 useEffect
的完全解读 --- A Complete Guide to useEffect
关于作者
Starry , Web 前端工程师,就职于民生银行后端平台研发团队,萤火虫实验室成员,目前负责仿真服务平台前端开发工作。