React函数组件为何不会重置状态?原理解析
这确实是React函数组件最容易让人困惑的点之一——明明每次状态变化时组件函数都会重新执行,看起来每次都在调用useState([])初始化状态,可状态却能持续累加,完全不会被重置。别担心,咱们一步步把这个“魔法”拆解开:
核心原理:React给组件分配了专属“状态内存”
React会为每个组件实例维护一个内部的“状态内存单元”列表,这些单元就像一个个独立的小存储盒,专门用来保存Hook的状态数据。当你调用useState时,React的逻辑是这样的:
- 首次渲染:把你传入的初始值存入对应的内存单元,然后返回这个初始值和对应的更新函数
- 后续重渲染:直接从对应的内存单元里读取当前的状态值,完全忽略你传入的初始值,然后返回当前值和更新函数
说白了,useState的初始值只是“第一次启动时的默认值”,后续渲染根本不会用到它——这就是状态不会被重置的关键!
结合你的代码例子看逻辑
先看你写的第一个按键记录组件:
export const KeyLogger = () => { const [keysLogged, setKeysLogged] = React.useState<string[]>([]); const logKey = (key: string) => { setKeysLogged(keysLogged.concat(key)); } console.log("HERE"); return ( <React.Fragment> <input onKeyUp={ev => logKey(ev.key)}/> <br/> <div>{keysLogged}</div> </React.Fragment> ); };
每次按键触发setKeysLogged更新状态,组件函数都会重新执行(所以每次都打印"HERE"),但useState([])这一行并不会把keysLogged重置为空数组:因为React已经在首次渲染时把空数组存入了对应的内存单元,后续渲染直接从这个单元读取最新的累加后的值,完全跳过了初始值[]。
为什么改变Hook调用顺序会出问题?
你后来测试的条件判断里调换useState顺序的例子,刚好验证了React Hook的另一个核心规则:Hook的调用顺序必须完全固定。
React是严格按照每次渲染时Hook的调用顺序来匹配内存单元的:比如第一次渲染时,第一个useState对应内存单元1,第二个对应单元2;如果下一次渲染时把两个useState的顺序调换了,那第一个调用就会去读单元2的值,第二个读单元1的值,直接打乱了状态的对应关系。
看你的测试代码:
let counter = 0; export const KeyLogger = () => { counter++; let keysLoggedA: any; let setKeysLoggedA: any; let keysLoggedB: any; let setKeysLoggedB: any; if (counter % 2 === 0) { [keysLoggedA, setKeysLoggedA] = React.useState<string[]>([]); [keysLoggedB, setKeysLoggedB] = React.useState<string[]>([]); } else { [keysLoggedB, setKeysLoggedB] = React.useState<string[]>([]); [keysLoggedA, setKeysLoggedA] = React.useState<string[]>([]); } const logKey = (key: string) => { setKeysLoggedA(keysLoggedA.concat(key)); } console.log("HERE"); return ( <React.Fragment> <input onKeyUp={ev => logKey(ev.key)}/> <br/> <div>{keysLoggedA}</div> <div>{keysLoggedB}</div> </React.Fragment> ); };
当counter是偶数时,第一个useState对应keysLoggedA的内存单元,第二个对应keysLoggedB的;当counter是奇数时,第一个useState去读keysLoggedB的单元,第二个读keysLoggedA的。所以你调用setKeysLoggedA时,实际更新的是当前顺序对应的内存单元,导致keysLoggedA和keysLoggedB的状态互相串了——这也是React官方反复强调Hook不能写在条件判断、循环或者嵌套函数里的原因,必须保证每次渲染时Hook的调用顺序完全一致。
最后总结一下
- 函数组件每次状态更新都会重新执行整个函数,但Hook的状态存在React维护的内存单元里,不是函数内部的局部变量
useState的初始值只在首次渲染时生效,后续渲染直接读取内存单元的当前值- Hook的调用顺序必须固定,否则会导致状态匹配错误
内容的提问来源于stack exchange,提问作者micah

