大家好,我是 小公主 👋。
最近在复盘面试题的时候,遇到了一道很有代表性的问题:手写一个防闭包陷阱的 useInterval。
别看只是个定时器问题,背后其实考察了 React Hooks 的核心逻辑:闭包陷阱、副作用管理、内存泄漏防范和可复用性。
在快手、字节、美团等大厂的前端面试里,这是一道高频题,能很好地区分“会用 Hooks”与“理解 Hooks”的差别。
今天这篇文章,我就带大家:
- 看看为什么直接写会踩闭包坑
- 手写一个健壮的
useInterval - 深入分析设计思想
- 给出完整的实战案例
读完这篇,你不仅能写,还能说清楚 为什么这么写。
🧩 一、为什么直接用 setInterval 会出问题?
初学者可能会写出这样的 Hook:
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id);
}, [delay]);
}
看似合理,但实际使用时会遇到 闭包陷阱(stale closure) 。
❌ 问题演示
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
console.log(count); // 期望打印最新值,但永远是 0!
}, 1000);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
原因在于:
- 第一次渲染时,
callback捕获了count = 0 - 组件更新后,
callback虽然变了,但useEffect只依赖了delay,不会重建定时器 - 结果
setInterval永远执行的是旧的回调
这就是典型的闭包问题。
✅ 二、正确解法:useRef + useEffect
解决思路很简单:
- 用
useRef存储最新的回调,突破闭包限制 - 用
useEffect管理定时器,在delay改变时重建
最终写法如下:
// hooks/useInterval.js
import { useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// 更新最新的回调
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// 管理定时器
useEffect(() => {
if (delay === null) return; // 传 null 表示暂停
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id); // 清理,防止内存泄漏
}, [delay]);
}
export default useInterval;
🔍 三、设计思想拆解
1. useRef 避免闭包陷阱
- 普通函数会捕获“值”,而
ref.current保存的是“引用” - 定时器每次执行的
savedCallback.current(),永远是最新的函数
2. 依赖分离,逻辑更清晰
callback更新时,只更新ref,不会重启定时器delay更新时,才重建定时器
这样避免了因为回调变化导致定时器被频繁清除重建。
3. 支持 null 来暂停
delay === null时,不创建定时器- 可以灵活实现“开始 / 暂停”的功能
4. 自动清理,防止内存泄漏
- 组件卸载时自动清除定时器
- 避免了后台还在执行的风险
🧪 四、实战:实现一个可暂停的计数器
import React, { useState } from 'react';
import useInterval from './hooks/useInterval';
function App() {
const [count, setCount] = useState(0);
const [running, setRunning] = useState(true);
useInterval(
() => setCount(prev => prev + 1),
running ? 1000 : null
);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setRunning(!running)}>
{running ? 'Pause' : 'Start'}
</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default App;
👉 运行效果如下图:
📚 五、为什么这是优秀实践?
| 特性 | 说明 |
|---|---|
| ✅ 解决闭包陷阱 | 保证回调永远拿到最新值 |
| ✅ 依赖分离 | 避免定时器频繁重建 |
| ✅ 支持暂停 | delay = null 灵活控制 |
| ✅ 自动清理 | 避免内存泄漏 |
| ✅ 可复用 | 组件化封装,逻辑清晰 |
这也是 React 官方推荐的实现方式,既优雅又健壮。
💡 面试加分点
如果在面试里遇到这道题,除了写出代码,还可以顺便补充:
- 为什么会产生闭包陷阱
- 为什么选择
useRef而不是useCallback - 如何防止内存泄漏
- 实际开发中的应用场景(轮询请求、动画、计时器等)
这样面试官会觉得你不仅能写,还能讲清楚原理。
🎯 总结
手写 useInterval 看似小题,但背后包含了 React Hooks 的核心理念:
- 闭包处理
- 副作用管理
- 可复用的自定义 Hook
掌握这道题,不仅能应对面试官的追问,更能在日常开发中写出健壮、可维护的业务逻辑。
下次面试官问:“你会手写 useInterval 吗?”
你就可以自信地说: “我不仅会写,还能讲明白为什么这么写。”
📌 如果你觉得文章对你有帮助,记得 点赞 + 收藏,也欢迎在评论区留言交流。
💬 你在面试中遇到过哪些有意思的 Hooks 手写题?一起来分享下吧 🚀