Effect Hook 可以让你在函数组件中执行副作用操作,类似于class组件中的
componentDidMount
,componentDidUpdate
和componentWillUnmount
这三种生命周期函数的组合
系列目录
- React新特性-Context(一)
- React新特性-Lazy和Suspense(二)
- React新特性-memo(三)
- React新特性-Hooks之useState(四)
- React新特性-Hooks之useEffect和useLayoutEffect(五)
- React新特性-Hooks之useContext(六)
- React新特性-Hooks之useRef(七)
- React新特性-Hooks之useMemo和useCallback(八)
- React新特性-Hooks之useReducer(九)
- React新特性-Hooks之自定义Hook(十完结)
什么是副作用
为了更好的理解副作用,首先我们先要知道什么是纯函数(Pure function)?
纯函数:一个函数的返回结果只依赖与它的参数,并且没有副作用,我们就把这个函数叫做纯函数。
- 函数的返回结果只依赖于它的参数
- 函数执行过程里面没有副作用
const a = 1
const foo = (b) => a + b
foo(2) // => 3
foo
函数不是一个纯函数,因为它返回的结果依赖于外部变量 a
,我们在不知道a
的值的情况下,并不能保证foo(2)
的返回值是 3。虽然foo
函数的代码实现并没有变化,传入的参数也没有变化,但它的返回值却是不可预料的,现在 foo(2)
是 3,可能过了一会就是 4 了,因为 a
可能发生了变化变成了 2。
const a = 1
const foo = (x, b) => x + b
foo(1, 2) // => 3
现在 foo
的返回结果只依赖于它的参数 x
和 b
,foo(1, 2)
永远是 3。今天是 3,明天也是 3,在服务器跑是 3,在客户端跑也 3,不管你外部发生了什么变化,foo(1, 2)
永远是 3。只要 foo
代码不改变,你传入的参数是确定的,那么 foo(1, 2)
的值永远是可预料的。
看到上面纯函数的介绍,大概对副作用也有些理解了。
副作用(Side Effect):是指一个函数做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log()
,所以 ajax
操作,修改 dom
都是算作副作用的;
如何使用useEffect
const TestInput= (props) => {
const [inputValue,setInputValue] = useState('请输入')
useEffect(()=>{
document.title = `标题 ${inputValue}`;
})
return (
<div>
<input value={inputValue} onChange={(e)=>setInputValue(e.target.value)} /> {inputValue}
</div>
)
}
export default TestInput
声明了inputValue state
变量,当DOM渲染完成后执行useEffect hook
函数。然后使用document.title
浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 inputValue
值,因为他在函数的作用域内。这个过程在每次渲染时都会发生(componentDidUpdate
),包括首次渲染(componentDidMount
)。每次重新渲染,都会生成新的 effect,替换掉之前的。
与
componentDidMount
或componentDidUpdate
不同,使用useEffect
调度的effect
不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。
需要清除的 effect
上面写的例子中不需要清除副作用,还有一些副作用是需要清除的。例如订阅外部数据源、定时器等。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!相当于class组件中componentWillUnmount
所要做的事情。
内存泄露:不再用到的内存,没有及时释放,就叫做内存泄漏
import React, { useState, useEffect } from 'react'
const OtherComponent = () => {
const [count, setCount] = useState(0);
//使用useEffect来执行副作用
useEffect(() => {
const timer = setInterval(() => {
setCount((count) => count + 1)
}, 1000)
return () =>clearInterval(timer)
})
return (
<div>
数量:{count}
</div>
)
}
export default OtherComponent
useEffect
可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数
React 何时清除 effect
React 会在组件卸载和组件每次重新渲染时都会执行。所以React会在执行当前effect之前对上一个 effect 进行清除
const TestInput= (props) => {
const [inputValue,setInputValue] = useState('请输入')
useEffect(()=>{
console.log('进入useEffect')
document.title = `标题 ${inputValue}`;
return ()=>console.log('清除 effect')
},[])
return (
<div>
<input value={inputValue} onChange={(e)=>setInputValue(e.target.value)} /> {inputValue}
</div>
)
}
export default TestInput

上图显示了初次渲染组件之后,进入到useEffect
函数中,接着每次输入setInputValue
之后都会再次进入到useEffect
函数中,但是在进入之前会先运行清除函数在进入到useEffect
中。
使用多个 Effect 实现关注点分离
上一章中我们说过Hook的优势,其中就有关注点分离解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
下面是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
});
}
// ...
可以发现在设置document.title
的逻辑被componentDidMount
和 componentDidUpdate
所分割,订阅逻辑又被componentDidMount
和componentWillUnmount
分割。而且 componentDidMount
中同时包含了两个不同功能的代码。
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
//document.title 的effect
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
//订阅逻辑effect
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
通过跳过 Effect 进行性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中通过在componentDidUpdate
中添加对prevProps
或prevState
的比较逻辑解决。
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
第二个参数是数组,如果数组中的其中一个值在重渲染之间发生变化,就会在次进入effect中。如果没有React 会跳过这个 effect,这就实现了性能的优化。
注意:这里的对比本次和上一次对比,也就是current count
===
prev count 。简单类型对比数值,复杂类型对比地址。
对于有清除操作的 effect 同样适用,同时也支持props传入的值进行对比,比如父组件频繁的渲染这时候就能利用到useEffect第二个参数来对比决定是否子组件要进入useEffect中。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
如果你只需要组件首次渲染才进入到useEffect中,可以设置useEffect第二参数为一个空的数组即可。这样做除了首次渲染会进入后面的渲染都不会在进入。
useEffect(()=>{
//首次会进入,以后每次渲染都不会进入,除非卸载后再次进入到该组件,才会再进入到useEffect中
},[])
useEffect和useLayoutEffect
useLayoutEffect
函数与useEffect
相同,但它会在所有的DOM
变更之后同步调用effect。
当你的useEffect
里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, 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;

可以清楚的看到有一个一闪而过的方块.
改成useLayoutEffect
试试
const Animate = () => {
const REl = useRef(null);
//变更为useLayoutEffect
useLayoutEffect(() => {
//下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置
TweenMax.to(REl.current, 0, {x: 600})
}, []);
return (
<div className='animate'>
<div ref={REl} className="square">square</div>
</div>
);
};

可以看出,每次刷新,页面基本没变化