【React】如何实现一个Interval Hook
useInterval(() => { // do something...}, 1000);复制代码
可能你看过也写过一些 react hook ,不过你对 hook 的种种行为真的了解吗?这篇文章为你剖析 hook 对比 class component “反常” 的那些事儿。
有了自带的 setInterval 为何还要再实现一个
its arguments are “dynamic”
可以注意到我们的 setInterval 是接受一个 dealy 值的, 并且这个值是可以由我们的代码控制的, 这意味着我们可以随时调整这个值来做动态的改变.
可以做到这样: 用一个 interval 控制另一个 interval 的速度
class component 实现
第一次尝试
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }); return{count}
;}复制代码
我们一开始一般会写出这样的实现, useEffect 设置 interval, return cleanup. 然而这样写会有个奇怪的表现...
react 在默认在每次 render 之后会重新执行 effects, 这其实也是 react 所预期的, 因为这样能避免 .
我们通常会使用 effect 来订阅, 退订一些 api, 但是在 setInterval 上使用的时候就会有问题, 因为执行 clearInterval
和 setInterval
是有时间差的, 当 react 渲染过于频繁的时候, 就会出现 interval 压根没机会执行的情况!
我们以 100ms 的频率去渲染 counter 组件,我们会发现 count 值一直没有更新
第二次尝试
在上一个阶段中, 我们的问题是重复执行 effects 导致了 interval 被清理的太早.
我们知道 useEffect 可以传入一个参数来决定是否重复执行 effects, 试一下
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return{count}
;}复制代码
好的, 现在我们 counter 更新到 1 就停止了
发生了什么?!
这其实是个很常见的闭包问题, 也有了对应的 .
我们的 effects 现在只会运行一次, 所以 effects 每次捕获的 count 值都是第一次 render 的 count 值(0), 所以 count + 1
一直是 1
有一种 fix 的方式是, 用 setState 的函数参数, setCount(count => count + 1)
, 这样我们就可以读取最新的 state, 但是这种方式不是万能的, 比如不能读取最新的 props, 那么假如我们需要根据最新的 props 来 setState 就无法实现了
使用 Refs
我们回到上个问题, count 无法被正确读取的原因是 count 的值一直引用的是第一次 render 的.
那如果我们在每次 render 的时候动态地改变 setInterval(fn, delay)
中 fn 函数, 使这个函数带上最新的 props 和 state, 并且这个 fn 函数要能在多次 render 之间可持续(persist), 这样 setInterval 执行的时候, 就可以实时的读取这个函数拿到最新的值了
第一版实现:
function setInterval(callback) { const savedCallback = useRef(); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { const tick = () => savedCallback.current(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, []);}复制代码
支持动态 delay
和 暂停
的最终版:
function setInterval(callback, delay) { const savedCallback = useRef(); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { const tick = () => savedCallback.current(); if (delay !== undefined) { const id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]);}复制代码
我们可以用这个 hook 做一些更加好玩的事 -- 用一个 interval 控制另一个 interval 的速度,就是一开始我们看到的那个动图的样子。
练习
function Counter() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { console.log(`Clicked ${count} times`); }, 3000); }); return [{count}
, ];}复制代码
猜猜打印结果?
看看 class component 的表现如何?
class Counter extends React.Component { state = { count: 0 }; componentDidUpdate() { setTimeout(() => { console.log(`Clicked ${this.state.count} times`); }, 3000); } render() { const { count } = this.state; return [{count}
, ]; }}复制代码
如何改造上面的 class component 让它跟使用 hook 的组件一样打印不同值?
hook 版本的怎么改能变得跟之前的 class component 一样打印相同值呢?
function Counter() { const [count, setCount] = useState(0); const saved = useRef(count); useEffect(() => { saved.current = count; setTimeout(() => { console.log(`Clicked ${saved.current} times`); }, 3000); }); return [{count}
, ];}复制代码
专栏其他文章
参考: