本文分享一个短小而又深刻的 React Hook 场景题,这个例子涉及到:
希望看完以后你会对 React 函数组件有更深入的了解。
场景复现
整个 Demo 非常简单,大家可以自己在电脑上尝试一下。
首先,有一个 button 和一个 list:
<div className="App">
<button onClick={add}>Addbutton>
{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}>Addbutton>
{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
问题
现在问题来了:
现在我们点击这些「数字按钮」,页面会怎么展示呢
-
比如点击 0,页面会如何展示,list 最终结果是什么
-
你可以先停下来思考一下,再继续往下读。
解答
有的同学可能会认为,点击「数字按钮」后,会有新的 button 被添加到 list 中。
先说结论,这个答案并不正确。
真正的现象是,点击数字按钮后:
-
-
并且列表最后一个数字会变成
点击之前最大的数字 + 1
。
文字不太容易理解,举一个🌰。
假设当前列表为:
我们点击
0
:
如果点击
2
:
为什么会这样呢?
原理剖析
造成这种反直觉现象的原因有两个:
-
-
再来看看点击按钮会调用的
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 函数实际上执行的是: