闭包陷阱指的是在 React 函数组件中,由于闭包的特性,回调函数或副作用函数捕获了旧的变量值,而不是最新的值。
function Counter() {
const [count, setCount] = useState(0);
// ❌ 问题:点击按钮时,回调函数使用的是创建时的 count 值
const increment = useCallback(() => {
setCount(count + 1); // 总是使用初始的 count 值
}, []); // 依赖数组为空,只创建一次
return <button onClick={increment}>Count: {count}</button>;
}
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ❌ 问题:总是输出 0,闭包捕获了初始值
console.log(time);
setTime(time + 1); // 只会从 0 加到 1,然后一直加 1
}, 1000);
return () => clearInterval(interval);
}, []); // 依赖数组为空,effect 只运行一次
return <div>Time: {time}</div>;
}
function Counter() {
const [count, setCount] = useState(0);
// ✅ 正确:将 count 添加到依赖数组
const increment = useCallback(() => {
setCount(count + 1);
}, [count]); // 依赖 count,count 变化时重新创建函数
return <button onClick={increment}>Count: {count}</button>;
}
function Counter() {
const [count, setCount] = useState(0);
// ✅ 最佳实践:使用函数式更新,无需依赖 count
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // 依赖数组可以为空
return <button onClick={increment}>Count: {count}</button>;
}
function Timer() {
const [time, setTime] = useState(0);
const timeRef = useRef(time);
// 同步 ref 和 state
useEffect(() => {
timeRef.current = time;
}, [time]);
useEffect(() => {
const interval = setInterval(() => {
// ✅ 正确:使用 ref 访问最新值
console.log(timeRef.current);
setTime(timeRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // 依赖数组可以为空
return <div>Time: {time}</div>;
}
// 自定义 hook 解决闭包问题
function useLatestRef(value) {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
function Timer() {
const [time, setTime] = useState(0);
const timeRef = useLatestRef(time);
useEffect(() => {
const interval = setInterval(() => {
console.log(timeRef.current);
setTime(timeRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Time: {time}</div>;
}
function Form() {
const [data, setData] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const dataRef = useRef(data);
useEffect(() => {
dataRef.current = data;
}, [data]);
// ✅ 使用 ref 或函数式更新
const handleSubmit = useCallback(async () => {
setIsSubmitting(true);
try {
// 可以安全访问最新的 data
await submitData(dataRef.current);
} catch (error) {
console.error("提交失败:", error);
} finally {
setIsSubmitting(false);
}
}, []); // 不需要依赖 data
return <form onSubmit={handleSubmit}>{/* 表单内容 */}</form>;
}
function ScrollComponent() {
const [scrollY, setScrollY] = useState(0);
const [isScrollingDown, setIsScrollingDown] = useState(false);
const prevScrollYRef = useRef(0);
const handleScroll = useCallback(() => {
const currentScrollY = window.scrollY;
setIsScrollingDown(currentScrollY > prevScrollYRef.current);
prevScrollYRef.current = currentScrollY;
setScrollY(currentScrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<div>
<div>滚动位置: {scrollY}</div>
<div>{isScrollingDown ? "向下滚动" : "向上滚动"}</div>
</div>
);
}
setCount(prev => prev + 1);// 调试 hook
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changes = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changes[key] = {
from: previousProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changes).length) {
console.log("[why-did-you-update]", name, changes);
}
}
previousProps.current = props;
});
}
理解闭包陷阱的关键是认识到:函数组件每次渲染都会创建一个新的闭包作用域,而 useCallback 可以帮助我们控制何时创建新的函数引用。