useEffect是hooks中又一个重要的函数。Effect hooks允许你在组件内部中执行副作用操作。
副作用包括:
useEffect就是为了处理这些副作用而被创造出来的函数,它相当于class中componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
useEffect(()=>{},[])
useEffect 有两个参数。第一个参数是Effect,第二个参数是依赖值列表。
组件的每一次渲染都会产生一个新的Effect,因为这个Effect每一次渲染拿到的prop和state是不同的。
第二次参数是依赖值列表。
在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
,因此会使得额外操作很方便。//执行一次
useEffect(()=>{
...
},[])
useEffect(()=>{
console.log("挂载时候执行")
return ()=>{
consloe.log("卸载的时候执行")
}
},[])
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]);
它的函数签名与useEffect相同,但它会在所有的DOM变更之后同步调用effect,可以使用它来读取DOM布局兵同步触发渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。尽可能使用标准的useEffect以避免阻塞视觉更新。
如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect
与 componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
如果你使用服务端渲染,请记住,无论 useLayoutEffect
还是 useEffect
都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect
代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。
若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild &&
进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
useEffect是异步执行,而且是在渲染被绘制到屏幕之后执行。
流程如下:
useLayoutEffect是同步执行,时机在渲染之后但在屏幕更新之前。
流程如下:
总结:
基本上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里面的函数执行完毕之后才会去渲染浏览器。