背景
我在现在这家公司,一直都是用 React
做开发的,对 React
的闭包陷阱也有一定的了解,但是要我解释为什么会有闭包陷阱这个问题,也知道解决闭包的方式,就是 setState
的时候,传一个函数就能解决,但是原理和为什么是这样却说不清楚。记得在一次面试中,面试官还问过我这个问题,当时也是支支吾吾,说不清楚。后来在搜索引擎上搜索 React 闭包陷阱相关的资料,出现的例子的 setInterval
和 setTimeout
,但是我日常开发中,很少使用这些东西,感觉很难理解。我一直记着这个问题,有机会就会回头再看看,最后看到一个比较好理解的例子,所以就做一次学习记录。
代码
代码还是很简单的,就是有一个全局变量 i
, 还有一个按钮,每次点击按钮,我们就往数组里头放一个按钮,按钮的上的文字就是 全局变量 i + 1
。i
只是用于记录文字,和闭包陷阱没有关系。
import { useState } from "react";
import type { ReactElement } from 'react';
let i = 0;
const DemoBtn = () => {
const [list, setList] = useState<ReactElement[]>([]);
const add = () => {
setList(list.concat(<button key={i} onClick={add}>{i++}</button>));
};
return (
<div className="App">
<button onClick={add}>Add</button>
{list.map((val) => val)}
</div>
);
};
export default function App() {
return <DemoBtn />;
}
UI
显示就如下图:
问题出现
一些看起来都是那么的正常,我点击 Add
按钮,他就自然的往后加按钮。但是如果你不小心,点了 1 ~ 7 中的其他按钮,比如我点击了 2 。结果
这。。。。是什么情况?你点击的数字会让数组的长度变成对应的数字。比如我点击了 2,数组的长度就变成了 2。
调试
我们不妨打上断点调试一下,我的调试结果是这样的,我点击的是 2 的按钮,结果就如图所示:
在这个闭包中,list
数组只有两个元素,如果你尝试别的数字,也是一样的。
分析
list
和 setList
是我们调用 useState()
返回的,我之前还以为,每次调用 useState
返回的是同一个 list
,或者说没有想过,会是不同的数据。但是实际上 useState
返回的 list
是基于 base state
计算出来的:
current state = base state + update1 + update2 + …
每次会将上一次的 state
与 update
进行合并得到新的 current state
。
拿上面的例子来说,画个图来表示他的闭包,应该是这样子的
解决方法
相信解决方法大家都知道,也很简单,把原来 add 中的 setList 方法改成下面这样就可以啦
const add = () => {
setList((btnList) => btnList.concat(<button key={i} onClick={add}>{i++}</button>));
};
总结
useState 返回的 list 是基于 base state 计算出来的,并且由于闭包的存在,每个「数字按钮」add 函数中的 list 都不同
有个问题得说明一下,这并不是 React
框架的问题,这是 JavaScript
闭包的特点,而 useState
是基于 base state
更新的,所以就会显示出一种和我们预期不太一样的结果。