为什么useRef可以解决React中闭包导致的stale state问题?
先戳破最核心的认知误区
很多人卡在这里,是默认把函数组件当成了常驻内存、随时更新内部变量的单例——但本质上,函数组件就是你写的一个普通JS函数:每次状态变化触发重渲染时,React会把这个函数从头到尾完整执行一遍,生成全新的一套局部作用域,所有在这个作用域里声明的变量(包括useState返回的count),都是当次执行独有的局部常量,和上一次执行生成的同名变量没有任何关联。
为什么直接引用count必然出现stale state
对着官方示例走一遍真实执行流程,就完全顺了:
- 组件第一次执行(初次渲染):生成局部作用域里的
count = 0,基于这个作用域创建的handleAlertClick回调,内部的setTimeout闭包直接锁定了当次作用域里的count值(也就是0)。这个版本的回调会被绑定到“Show alert”按钮的点击事件上。 - 你点击“Show alert”,把这个锁着count=0的setTimeout定时器注册到浏览器的事件队列里,等3秒后执行。
- 之后你点击“Click me”触发状态更新,组件会被第二次完整执行:这次会生成全新的局部作用域,新的
count = 1,基于新作用域创建的全新handleAlertClick会替换掉之前绑在按钮上的旧回调——但之前已经注册进队列的那个setTimeout定时器不会被替换,它的闭包还牢牢锁着第一次执行时的count=0。等3秒到了,它自然弹出锁死的0,这就是大家常说的stale state。
为什么useRef能绕开这个问题
核心原因是:useRef返回的对象,从根源上就不属于某一次组件渲染的局部作用域。
你可以把useRef理解成React给你存在组件实例内存里的一个共享盒子:不管组件被执行多少次,你每次调用useRef()拿到的都是这个同一个盒子的引用,不是当次执行新生成的局部变量。
对应改造后的代码,流程就变了:
- 初次渲染时,你拿到这个跨所有渲染共享的
stateRef盒子,执行stateRef.current = count,把盒子里存的值设为0。这时候创建的setTimeout闭包,锁定的不是盒子里当时存的0值,而是这个共享盒子的引用本身。 - 你点击“Show alert”注册定时器,回调里攥着的是盒子的地址,它没有锁死任何具体的数字。
- 后续触发重渲染时,组件第二次执行,还是拿到那个一模一样的共享
stateRef盒子,执行stateRef.current = count,把盒子里存的值从0改成1。等3秒后定时器触发,它顺着攥着的盒子地址去读里面的current属性,拿到的就是刚被更新的最新值1。
为什么同词法作用域下,两个变量的表现完全不一样
你觉得奇怪,是因为看到count和stateRef都写在Example函数的顶层,以为是同层级的普通变量,但二者的本质天差地别:
count是当次渲染新生成的局部常量,闭包锁的是常量本身的值:一旦创建就不会变化,后续渲染生成的新count,和之前闭包攥着的旧count没有任何关系。stateRef是跨渲染共享的可变容器的引用,闭包锁的是容器本身的地址:你每次渲染更新的只是容器内部存的current字段,从来没换过容器的地址,所以之前攥着地址的旧闭包,自然能读到容器里最新的存值。
补个最容易踩的认知坑:闭包从来不会“实时跟踪”外层变量的变化,它只会牢牢记住创建回调时,作用域链上能拿到的变量的引用。如果这个引用指向的是不会变的局部常量,就会出现stale state;如果这个引用指向的是跨渲染共享的可变对象,就永远能拿到最新值。
内容的提问来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

