本文分享一个短小而又深刻的 React Hook 场景题,这个例子涉及到:
希望看完以后你会对 React 函数组件有更深入的了解。
整个 Demo 非常简单,大家可以自己在电脑上尝试一下。
首先,有一个 button 和一个 list:
<div className="App">
<button onClick={add}>Add</button>
{list.map(val => val)}
</div>
list 是使用 useState 管理的状态。button 绑定了事件 onClick={add}
。
点击按钮,会执行 add
方法向 list 中加入一些内容。
export default function App() {
const [list, setList] = useState([]);
const add = () => {
// ...
};
return (
<div className="App">
<button onClick={add}>Add</button>
{list.map(val => val)}
</div>
);
}
现在页面看起来像这样:
我们继续,先在 App 外部定义变量 i
。
let i = 0;
export default function App() {
// ...
}
接着重点来看看 add
方法。
调用 add,会向 list 中添加新的 button,新 button 也绑定了 onClick={add}
。
const add = () => {
setList(
list.concat(
<button
key={i}
onClick={add}>
{i++}
</button>
)
);
};
当我们点击「Add 按钮」7 次,会展示:
在线示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js
现在问题来了:现在我们点击这些「数字按钮」,页面会怎么展示呢
你可以先停下来思考一下,再继续往下读。
有的同学可能会认为,点击「数字按钮」后,会有新的 button 被添加到 list 中。
先说结论,这个答案并不正确。
真正的现象是,点击数字按钮后:
点击的数字 + 1
点击之前最大的数字 + 1
。文字不太容易理解,举一个🌰。
假设当前列表为:
我们点击 0
:
0
+ 1 = 16
+ 1 = 7如果点击 2
:
2
+ 1 = 36
+ 1 = 7为什么会这样呢?
造成这种反直觉现象的原因有两个:
再来看看点击按钮会调用的 add
函数:
const add = () => {
setList(
list.concat(
<button
key={i}
onClick={add}>
{i++}
</button>
)
);
};
当执行 add 函数时,由于访问了外层函数 App 内的变量,所以会根据 App 函数上下文形成闭包,闭包内包括:
list 和 setList 是调用 useState() 返回的。
这里通常有一个误解:多次调用 useState,返回的 list 都是同一个对象。
实际上,useState 返回的 list 都是基于 base state 计算出来的:
current state = base state + update1 + update2 + …
每次会将上一次的 prev state 与 update 进行合并得到新的 current state。
因此,每次调用 useState 返回的 list 都不是同一个对象,它们的内存地址不同。
这会导致每个「数字按钮」的 add
函数处于不同的闭包中,每个闭包当中的 list 都不同。
而变量 i
是声明在 App 外层的模块级变量,在每个闭包中 i
都是相同的。
let i = 0;
export default function App() {
// ...
}
所以,在点击 0
时:
add 函数实际上执行的是:
setList(
[].concat(
<button key={7} onClick={add}>{7}</button>
)
);
所以 list 最终变成了 [7]。
当点击 2
时:
add 函数实际上执行的是:
setList(
[0, 1].concat(
<button key={7} onClick={add}>{7}</button>
)
);
所以 list 最终变成了 [0, 1, 7]。
为了方便理解,这里的 [0, 1, 7] 省略了外层的
<button>
标签
那么如何解决这个闭包问题,在 list 后面正常拼接 button 呢?
很简单,只要将 list 从闭包中清理出去就可以了,将 setList 参数改为函数形式。
之前是:
setList(
list.concat(
<button
key={i}
onClick={add}>
{i++}
</button>
)
);
修改为:
setList(list =>
list.concat(
<button
key={i}
onClick={add}>
{i++}
</button>
)
);
这样,我们点击「Add 按钮」或任意「数字按钮」都会正常在 list 后面拼接新按钮。
大家可以通过在线示例来加深理解。
在线示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js
由于 state 的更新机制是:
current state = base state + update1 + update2 + …
所以每次调用 useState,返回值 list 都是不同的对象。
并且由于闭包的存在,每个「数字按钮」add 函数中的 list 都不同。
两者共同作用,造成了这种不符合直觉的现象。
如何解决这种闭包问题呢?我们可以将 setState 改为函数形式,将变量从闭包中清理出去。
如果觉得有用,就点赞、在看、分享吧,谢谢大家啦~