一、介绍
useEffect是hooks中又一个重要的函数。Effect hooks允许你在组件内部中执行副作用操作。
副作用包括:
- 数据获取
- 设置订阅
- 手动更改DOM等等
useEffect就是为了处理这些副作用而被创造出来的函数,它相当于class中componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
二、基本使用
useEffect(()=>{},[])
useEffect 有两个参数。第一个参数是Effect,第二个参数是依赖值列表。
组件的每一次渲染都会产生一个新的Effect,因为这个Effect每一次渲染拿到的prop和state是不同的。
第二次参数是依赖值列表。
- 当不传递的时候,useEffect会在每次渲染都执行
- 当传递一个[]的时候,那么useEffect只会在挂载的时候执行(如果useEffect返回了一个函数,那么这个函数会在组件卸载的时候执行)
- 当传递一个值的时候,如[a],那么useEffect就会根据这个值是否发生变化,来判断是否去执行useEffect函数
- 当传递多个值的时候,如[a,b,c…],那么他就是会比较每一个值,有一个不相同就回去执行
三、两种副作用
在React中有两种常见的副作用操作:需要被清除和不需要被清除的。
无需清除的副作用:
有时候,我们只想在React和更新DOM操作之后运行一些额外的代码。比如说,发送网络请求、手动变更DOM、记录日志,这些都是常见的无需清除副作用的操作。因为我们在执行完这些操作之后,就可以忽略他们了。
class实现:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
hooks实现:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
从上述的代码中可以看出:
在class组件中 在俩个生命周期函数中都设置了domcument.title,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
而在hooks中只是在useEffect函数的回调函数中设置了一次document.title。
-
useEffect
做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
-
为什么在组件内部调用
useEffect
? 将
useEffect
放在组件内部让我们可以在 effect 中直接访问count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。 -
useEffect
会在每次渲染后都执行吗? 默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
上述代码中,我们声明了
count
state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给useEffect
Hook。此函数就是我们的 effect。然后使用document.title
浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的count
值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。 传递给
useEffect
的函数在每次渲染中都会有所不同,也就是useEffect的第一个参数。这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的count
的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染 **注意:**与
componentDidMount
或componentDidUpdate
不同,使用useEffect
调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快(用户体验好)。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect
Hook 供你使用,其 API 与useEffect
相同。
需要清除的副作用
- 订阅外部的数据源
- 这种情况下,清除副作用是十分重要的,可以防止内存泄露
Class:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
}
handleStatusChange(status) {
this.setState({isOnline: status.isOnline});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
Hooks:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
在class组件中,我们需要在componentDidMount生命周期函数中发起订阅,然后在componentWillMount生命周期中取消订阅。他们是相对应的,使得生命周期函数迫使我们拆分这些逻辑代码,即使这两部分都作用于相同的副作用。
而在hooks中,useEffect就看起来直观多了。在useEffect函数中返回一个函数就可以做到清除副作用。
为什么要在effect函数中返回一个函数?
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除effect?
React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
**注意:**并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup
是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
使用多个effect可以分离关注点。
在class组件有多个副作用函数的时候,这些副作用函数会按照生命周期的不同,将代码逻辑分割放到不同的生命周期函数中。
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
而如果在hooks中,它允许我们允许我们按照代码的用途分离他们,这样我们就可以将关注点分离,各自维护各自的代码逻辑。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
为什么每次更新都要运行useEffect函数:
这是因为在class组件中当props发生变化的时候,组件会继续展示原来的样子,而且因为在componentWillMount生命周期函数中做的操作导致内存泄漏。所以我们还需要一个componentDidUpdate函数来解决这个问题。但是在useEffect hooks函数中完全不必担心这些问题。并不需要特定的代码来处理更新逻辑,因为 useEffect
默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。
跳过Effect进行性能优化:
讲到性能优化,我们就要说说useEffect的第二参数,一个参数列表。我们知道在class组件中我们可以通过在 componentDidUpdate
中添加对 prevProps
或 prevState
的比较逻辑解,去做性能优化,这是很常见的需求,所以它被内置到了 useEffect
的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect
的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
上述代码表示,这个useEffect函数的执行需要依赖于count值,如果count值没有发生变化,那么通知到React,跳过对该该函数的执行。
- 如果希望useEffect只执行一次,也就是只在挂载的时候执行。那么我们就可以传递一个空数组作为第二个参数。这就告诉React不依赖于任何任何props和state,所以它永远都不会重复执行。effect 内部的 props 和 state 就会一直拥有其初始值,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用
useEffect
,因此会使得额外操作很方便。
//执行一次
useEffect(()=>{
...
},[])
- 如果想useEffect,只在挂在和卸载时执行。那么可以给useEffect返回一个函数。return返回这个函数叫做清理函数,通常用于清除一些这个清理函数会在组件卸载的时候执行。
useEffect(()=>{
console.log("挂载时候执行")
return ()=>{
consloe.log("卸载的时候执行")
}
},[])
- 如果想使用useEffect获取数据
function Reddit() {
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
"https://www.reddit.com/r/reactjs.json"
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
}); // 这里没有传入第二个参数,你猜猜会发生什么?
注意到咱们没有将第二个参数传递给useEffect
,在语法上可行,但在开发中,不能这样做。
不传递第二个参数会导致每次渲染都会运行useEffect
。然后,当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发useEffect
,这就是问题所在。
为了解决这个问题,我们需要传递一个数组作为第二个参数,数组内容又是啥呢。
useEffect
所依赖的唯一变量是setPosts
。因此,咱们应该在这里传递数组[setPosts]
。因为setPosts
是useState
返回的setter
,所以不会在每次渲染时重新创建它,因此effect
只会运行一次。
接着扩展一下示例,以涵盖另一个常见问题:如何在某些内容发生更改时重新获取数据,例如用户ID,名称等。
首先,咱们更改Reddit
组件以接受subreddit
作为一个prop,并基于该subreddit
获取数据,只有当 prop
更改时才重新运行effect
.
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
`https://www.reddit.com/r/${subreddit}.json`
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
// 当`subreddit`改变时重新运行useEffect:
}, [subreddit, setPosts]);
四、useLayoutEffect
它的函数签名与useEffect相同,但它会在所有的DOM变更之后同步调用effect,可以使用它来读取DOM布局兵同步触发渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。尽可能使用标准的useEffect以避免阻塞视觉更新。
如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect
与 componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
如果你使用服务端渲染,请记住,无论 useLayoutEffect
还是 useEffect
都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect
代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。
若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && <Child />
进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
五、区别详解
useEffect是异步执行,而且是在渲染被绘制到屏幕之后执行。
流程如下:
- 你以某种方式触发了rerender(改变state,或者父组件发生rerender)
- React渲染你的组件(调用组件函数)
- 屏幕在视觉上更新(真实dom操作)
- 然后useEffect运行
useLayoutEffect是同步执行,时机在渲染之后但在屏幕更新之前。
流程如下:
- 你以某种方式触发了rerender(改变state,或者父组件发生rerender)
- React渲染你的组件(调用组件函数)
- useLayoutEffect运行,React等待它完成
- 屏幕在视觉上更新(真实dom操作)
总结:
基本上90%的情况下,都应该用useEffect,这个是在render结束后,你的callback函数执行,但是不会影响浏览器的绘制,是异步的。但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.
useLayoutEffect是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用useLayoutEffect,否则可能会出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.
import React, { useEffect, useLayoutEffect, useRef } from "react";
import TweenMax from "gsap/TweenMax";
import './index.less';
const Animate = () => {
const REl = useRef(null);
useEffect(() => {
/*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
TweenMax.to(REl.current, 0, {x: 600})
}, []);
return (
<div className='animate'>
<div ref={REl} className="square">square</div>
</div>
);
};
export default Animate;
上述代码在运行的时候,会看到一个一闪而过的方块。说明组件先会render一次,然后再去执行useEffect里面的代码。改成useLayoutEffect之后,浏览器会等到useLayoutEffect里面的函数执行完毕之后才会去渲染浏览器。
- useLayoutEffect总是比useEffect先执行
- useLayoutEffect里的任务最好影响了Layout
- 为了用户体验,优先使用useEffect(优先渲染)