今天学习另一个
Hook: useEffect
,它能在函数组件中执行副作用,与
class
中的生命周期函数极为类似。
useEffect
useEffect Hook
可以看做componentDidMount,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
react
首次渲染和之后的每次渲染都会调用一遍传给useEffect
的函数。之前要用两个声明周期函数来分别表示首次渲染(componentDidMount)
,和之后的更新导致的重新渲染(componentDidUpdate)
。
useEffect
中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,之前的componentDidMount
或componentDidUpdate
中的代码则是同步执行的。
例子:
import React, { useState, useEffect } from 'react'
function Example () {
const [count, setCount] = useState(0)
const [age, setAge] = useState(42)
useEffect(() => {
// 将 document 的 title 设置为包含了点击次数的消息
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<p>setAge: {age} </p>
<button onClick={() => setAge(age + 3)}>
Click me
</button>
</div>
)
}
export default Example
等价于:
import React from 'react'
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
age: 10
};
}
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>
<p>setAge: {this.state.age} </p>
<button onClick={() => this.setState({
age: this.state.age + 3
})}>
Click me
</button>
</div>
);
}
}
export default Example
页面效果:
在 effect
中可以直接访问 count state
变量(或其他 props
),它已经保存在函数作用域中。(Hook
使用了 JavaScript
的闭包机制)
默认情况下,effect
在第一次渲染之后和每次更新之后都会执行。effect
发生在“渲染之后”,可以不用再去考虑“挂载”还是“更新”。
当 React
渲染组件时,会保存已使用的 effect
,并在更新完 DOM
后执行它。每次重新渲染,都会生成新的 effect
,替换掉之前的。(也就是说,每个 effect
“属于”一次特定的渲染)
清除 effect
如果
effect
返回一个函数,React
将会在执行清除操作时调用它。
每个 effect
都可以返回一个清除函数,可以将添加和移除订阅的逻辑放在一起。它们都属于 effect
的一部分。 React
会在组件卸载的时候执行清除操作。
例子:
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => {
// 执行清除操作
}
})
不必做清除操作的 effect
所以不需要返回函数,例如:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
多个effect
Hook
允许按照代码的用途分离他们, 而不是像生命周期函数那样。
useEffect(() => {
document.title = `You clicked ${count} times`;
})
useEffect(() => {
console.log('倒计时完毕!')
})
React
将按照 effect
声明的顺序依次调用组件中的每一个 effect
。
useEffect
进行性能优化
跳过 effect
如果某些特定值在两次重渲染之间没有发生变化,可以通知 React
跳过对 effect
的调用,只要传递数组作为 useEffect
的第二个可选参数即可:
useEffect(() => {
// 将 document 的 title 设置为包含了点击次数的消息
document.title = `You clicked ${count} times`;
}, [count]) // 仅在 count 更改时更新
上面这个例子中,传入 [count]
作为第二个参数,来实现性能优化。
具体实现:
如果 count
的值是 5,而且组件重渲染的时候 count
还是等于 5,React
将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React
会跳过这个 effect
,这就实现了性能的优化。
例子:
import React, { useState, useEffect } from 'react';
let timer; //声明定时器
const Example = () => {
const [current, setCurrent] = useState(0)
const [time, setTime] = useState(5)
useEffect(() => {
return () => {
clearInterval(timer)
}
}, [])
// 想执行只运行一次的 effect(仅在组件挂载和卸载时执行),传递一个空数组([])作为第二个参数。
// 告诉 React,effect 不依赖于 props 或 state 中的任何值,永远都不需要重复执行。
useEffect(() => {
if (current === 1) {
runTimerJump()
}
}, [current])
useEffect(() => {
if (time === 0) {
clearInterval(timer)
setTime(5)
alert('倒计时完毕!')
console.log('倒计时完毕!')
}
}, [time])
const runTimerJump = () => {
timer = setInterval(() => setTime(t => --t), 1000)
}
return (
<>
<p>
<span>{time}s 后执行对应的操作</span>
<br />
</p>
<p><button onClick={() => setCurrent(1)}>点击开启</button></p>
</>
)
}
export default Example
页面效果:
仅执行一次的 effect
以上例子中,useEffect
中 的 return
是返回来一个函数,来清除定时器:
useEffect(() => {
return () => {
clearInterval(timer)
}
}, [])
// 想执行只运行一次的 effect(仅在组件挂载和卸载时执行),传递一个空数组([])作为第二个参数。
// 告诉 React,effect 不依赖于 props 或 state 中的任何值,永远都不需要重复执行。
传递一个空数组([])作为第二个参数,effect
内部的 props
和 state
会一直拥有其初始值,告诉 React
,effect
不依赖于 props
或 state
中的任何值,永远都不需要重复执行。
适用情形:想执行只运行一次的 effect
(仅在组件挂载和卸载时执行)。