👋 Hello 前端人!
今天我们不聊八股文,不聊性能优化,
我们聊聊一个"每个 React 人都会遇到,但又容易被劝退"的问题------定时器在 React 中的正确打开方式!
🧠 一、先来回忆下那个老朋友:setInterval
在原生 JS 里,我们经常这样写:
js
setInterval(() => {
console.log('每秒执行一次~');
}, 1000);
这没啥问题,
但是如果你把它搬进 React 组件里------
🤯 就可能翻车!
比如:
js
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log('count:', count);
setCount(count + 1);
}, 1000);
}, []);
}
你以为每秒加一?
结果:它只会一直输出 count=0 😱
为什么?因为闭包!
回调函数捕获的是"当时的 count 值",并不会更新。
所以 setInterval 里的函数其实拿的是"老的状态"!
这时候,React 新手就开始懵逼三连:
- "为啥不更新?"
- "我是不是忘了加依赖?"
- "这 useEffect 是不是坏了?"
其实都没坏,是我们和闭包斗了个寂寞。
🧩 二、来点优雅的:自定义 Hook ------ useInterval
要彻底解决这个问题,我们可以封装一个属于自己的 Hook。
目标:
- 能正常执行 callback,不被闭包坑
- 能自由控制延迟(甚至暂停)
看代码
js
import { useEffect, useRef } from 'react';
function useInterval(callback, delay) {
// 用 useRef 保存 callback 引用
const savedCallback = useRef();
// 每次 callback 变化时更新 ref
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// 启动定时器 + 清除逻辑
useEffect(() => {
if (delay === null) return; // 支持暂停
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
export default useInterval;
⚙️ 三、逐行拆解(useRef 是关键!)
useRef
的真正作用是什么?
很多人以为 useRef
只是用来"获取 DOM 引用"。
其实不止如此!
useRef
是一个可变的盒子(Mutable Box) 。
你往里塞什么,它都不会因为重新渲染而丢掉。
简单理解:
ini
const box = useRef();
box.current = '我在 React 的所有重渲染中永存!';
所以在 useInterval
中:
ini
savedCallback.current = callback;
这一步就像把最新的 callback 放进一个"保险柜"里。
即使组件重新渲染,定时器里的函数永远能拿到最新的 callback。
四、那怎么实现"暂停"?
我们注意这行:
ini
if (delay === null) return;
这其实是一个非常巧妙的小技巧!✨
当你传入 delay = null
时,
useEffect
会直接跳过创建定时器的逻辑。
这样,定时器相当于被"暂停"了。
你可以这样用👇
js
function App() {
const [count, setCount] = useState(0);
const [delay, setDelay] = useState(1000);
useInterval(() => setCount(c => c + 1), delay);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setDelay(1000)}>开始</button>
<button onClick={() => setDelay(null)}>暂停</button>
</div>
);
}
点击"暂停"按钮时,delay
变为 null
,
useEffect
检测到变化 → 不再创建 interval → 定时器被清除!💥
🧙♂️ 五、为什么不直接写在组件里?
因为如果你直接在组件中使用 setInterval
,
- 每次组件重新渲染都会重新创建 interval
- callback 会被闭包锁死
- 清除不及时还可能内存泄漏
而封装成 useInterval
后:
- 逻辑独立、易复用
- 自动清理旧定时器
- 支持最新状态 + 可暂停
- 不用每次重新写一大坨 useEffect
可谓"一 Hook 在手,天下我有"。
🎨 六、扩展:实现"开始/暂停/重置"全家桶
来个加强版:
jsx
import { useRef, useEffect, useState } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
const [active, setActive] = useState(true);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (!active || delay === null) return;
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay, active]);
return {
pause: () => setActive(false),
resume: () => setActive(true),
toggle: () => setActive(a => !a),
};
}
现在你可以在组件里这样玩:
jsx
function Timer() {
const [count, setCount] = useState(0);
const { pause, resume, toggle } = useInterval(() => setCount(c => c + 1), 1000);
return (
<div>
<h3>计数:{count}</h3>
<button onClick={pause}>暂停</button>
<button onClick={resume}>继续</button>
<button onClick={toggle}>切换</button>
</div>
);
}
轻轻一点,秒变控制台!
从 setInterval 到自定义 Hook,你的代码从"混乱"变"优雅"🌈。
🧭 七、总结:这篇文章你得到了什么?
概念 | 含义 | 你应该记住的点 |
---|---|---|
setInterval | 原生定时器 | React 内使用容易被闭包坑 |
useRef | 可变引用盒子 | 保存最新 callback,不随渲染重置 |
useEffect | 副作用钩子 | 启动 + 清除定时器 |
delay=null | 暂停机制 | 通过跳过 useEffect 实现暂停 |
自定义 Hook | 封装逻辑 | 让组件更干净、更复用 |
🐣 尾声:从"小钩子"看大世界
React 的世界里,一切都是 Hook。
useState
、useEffect
是基础,
但当你开始学会封装自己的 useXxx
,
你就真的在迈向进阶 React 开发者的道路上了!
如果你想进一步拓展,我推荐你可以试试:
- 用
useInterval
做一个"打卡倒计时"组件; - 或者封装
useTimeout
; - 甚至做一个"多定时任务管理器"。
💬 最后,求个三连支持(点赞 + 收藏 + 评论)!
因为掘金的算法告诉我------
你点得越多,我就越像个懂前端的博主 😎