useEffect使用完全指南:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
- 先点击【延迟获取 count 值】按钮
- 立即点击【+1】按钮 3 次
问题:定时器打印的 count 值为多少?
提示:点击【延迟获取 count 值】按钮,创建定时器时,当前的状态值 count 是多少(组件是第几次更新)?
const App = () => {
const [count, setCount] = useState(0)
// 3 秒后,获取 count 值
const getCount = () => {
setTimeout(() => {
console.log(count)
}, 3000)
}
// 计数器 +1
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>+1</button>
<button onClick={getCount}>延迟获取 count 值</button>
<h1>计数器:{count}</h1>
</div>
)
}
问题:这种方式能正确清理定时器吗?
操作过程如下:
- 先点击【+1】按钮 1 次,让组件重新更新
- 再点击【清理定时器】按钮
分析该问题的出发点:clearInterval
的 timerId
和 useEffect
中的 timerId
是不是同一个?(提示:可以通过打印的方式,查看两处 timerId 的值)
const App = () => {
const [count, setCount] = useState(0)
let timerId = -1
useEffect(() => {
timerId = setInterval(() => {
console.log('interval')
}, 1000)
}, [])
const clear = () => {
clearInterval(timerId)
}
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>+1</button>
<button onClick={clear}>清理定时器</button>
<h1>计数器:{count}</h1>
</div>
)
}
因此,要想在组件更新后清理定时器,就需要让两处的 timerId
值是同一个,也就是要保持 timerId
的值在组件更新期间保持不变。此时,就用到:useRef Hook
了。
// 创建 ref 对象
const timerRef = useRef(-1)
useEffect(() => {
// 将定时器id存储到 ref 对象中
timerRef.current = setInterval(() => {
console.log('interval')
}, 1000)
}, [])
const clear = () => {
// 从 ref 对象中拿到之前存储的定时器id
clearInterval(timerRef.current)
}
// 说明:两个地方拿到的 timerRef.current 是同一个对象!
对于倒计时的定时器来说,只需要在组件创建时,开启一次即可。为了做到这一点,可以通过 useEffect(() => {}, [])
来实现
注意:此处的关键点是依赖项参数为:[](空数组)。因此,不能在 effect 回调函数中,依赖外部的数据
但是,页面中的计数器数值又要更新,因此就会有一个新的问题:
如何在不依赖于外部数据的情况下,在 effect 回调中,更新状态?
答:使用回调函数形式的setState来更新状态
const [count, setCount] = useState(0)
// 语法一:
setCount(count + 1)
// 语法二:回调形式的更新状态
setCount(prevCount => prevCount + 1)
对比以上两种语法的不同点:
// 语法一: 依赖外部数据,需要通过 useEffect 的第二个参数指定
useEffect(() => {
setCount(count + 1)
}, [count])
// 语法二: 不依赖于外部数据,不需要指定 useEffect 的依赖
useEffect(() => {
setCount(prevCount => prevCount + 1)
}, [])