React Hooks 之设计严重依赖于闭包,使用 hooks 时容易遇到过时闭包的问题。
每一个JS模块都可以认为是一个独立的作用域,当代码执行时,该词法作用域创建执行上下文,如果在模块内部,创建了可供外部引用访问的函数时,就为闭包的产生提供了条件,只要该函数在外部执行访问了模块内部的其他变量,闭包就会产生。
JS 中闭包
常见场景
js
for ( var i=0; i<5; i++ ) {
setTimeout(()=>{
console.log(i)
}, 0)
}打印结果,都是 5。回调函数是在循环结束后才会被执行。
如何解决这种问题,一种方法就是使用闭包。
js
for ( var i=0; i<5; i++ ) {
(function(i){
setTimeout(()=>{
console.log(i)
}, 0)
})(i)
}定时器的回调函数去引用立即执行函数里定义的变量,形成闭包保存了立即执行函数执行时 i 的值,异步定时器的回调函数才如我们想要的打印了顺序的值。
Hooks 中的闭包
函数组件中,组件第一次渲染的时候,为每个 hook 都创建一个对象。对象中的 next 指向下一个 hook。依次如此,最终形成链表。
ts
export type Hook = {
memoizedState: any, // 最新的状态值
baseState: any, // 初始状态值
baseQueue: Update<any, any> | null,
queue: UpdateQueue<any, any> | null, // 环形链表,存储的是该 hook 多次调用产生的更新对象
next: Hook | null, // next 指针,之下链表中的下一个 Hook
};hook 中的场景:
jsx
function Counter(){
const [count, setCount] = useState(1);
useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 1000)
}, [])
function click(){ setCount(2) }
}具体过程如下:
- 第一次渲染执行 Counter,useState 设置 count 初始状态为 1。
- 执行 useEffect ,回调函数执行,设置定时器每 s 打印 count。
- click 触发后,调用 setCount,触发 react 更新,更新到当前组件的时候继续执行 Counter。
- 链表已经形成,useState 将 Hook 对象上保存的状态值置为 2,count 变为 2。
- 继续执行到 useEffect, 依赖数组为空,回调不执行。
- 第二次更新没有涉及到定时器,定时器还在继续执行,count 仍然是第一次渲染时的值 1。count 在定时器的回调函数里被引用了,形成了闭包一直被保存。
闭包就像是一个照相机,把回调函数执行的那个时机的那些值保存了下来
解决闭包办法
方法一:
闭包中使用的变量添加到依赖项
方法二:
用函数的方式更新 state
jsx
setCount(alwaysActualStateValue => newStateValue);useRef 每次都能拿到最新值的原因
在组件每一次渲染的过程中,ref = useRef() 返回的都是同一个对象,每次组件更新所生成的 ref 指向的都是同一片内存空间。
References
纠错与补充
- 闭包里读到的 state 总是创建该闭包那次渲染的值,跟
setInterval、Promise、requestAnimationFrame等异步 API 无关;真正的问题在于我们没有把“最新值”显式存放在 ref 或 reducer 中。 - 在定时器或订阅回调里更新 state,优先使用函数式
setState(prev => ...),这样即便闭包捕获了旧的prev,React 也会把你传入的函数放到更新队列中按顺序执行,避免“越加越慢”的假象。 - React 19 引入的
useEvent(以及社区useEventCallback)本质上就是对文中useRef模式的封装,如果是在事件处理器中读取最新值,优先采用它可以减少手写样板代码。