专栏名称: 前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
目录
相关文章推荐
中国基金报  ·  今天,又爆了! ·  2 天前  
中国基金报  ·  《哪吒2》破81亿!冲入全球动画电影前10, ... ·  3 天前  
51好读  ›  专栏  ›  前端从进阶到入院

一道有挑战性的 React Hook 场景题,考考你的功底

前端从进阶到入院  · 公众号  ·  · 2022-07-16 08:00

正文

本文分享一个短小而又深刻的 React Hook 场景题,这个例子涉及到:
  • hook 闭包问题
  • state 更新机制

希望看完以后你会对 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 最终结果是什么
  • 点击 6,又会如何展示

你可以先停下来思考一下,再继续往下读。

解答

有的同学可能会认为,点击「数字按钮」后,会有新的 button 被添加到 list 中。

先说结论,这个答案并不正确。

真正的现象是,点击数字按钮后:

  • 列表的长度将会变成 点击的数字 + 1
  • 并且列表最后一个数字会变成 点击之前最大的数字 + 1

文字不太容易理解,举一个🌰。

假设当前列表为:

我们点击 0

  • 列表的长度会变成 0 + 1 = 1
  • 列表最后一个数字会变成 6 + 1 = 7

如果点击 2

  • 列表长度会变成 2 + 1 = 3
  • 列表最后一个数字会变次 6 + 1 = 7

为什么会这样呢?

原理剖析

造成这种反直觉现象的原因有两个:

  1. hook 闭包问题
  2. state 更新机制

再来看看点击按钮会调用的 add 函数:

const add = () => {
  setList(
    list.concat(
      <button 
        key={i} 
        onClick={add}>

        {i++}
      button>

    )
  );
};

当执行 add 函数时,由于访问了外层函数 App 内的变量,所以会根据 App 函数上下文形成闭包,闭包内包括:

  • add 函数
  • list 变量
  • setList 方法

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 时:

  • i 是模块级变量,值为 6
  • list 是闭包中的变量,值为 []

add 函数实际上执行的是:

setList(
  [].concat(
    <button key={7} onClick={add}>{7}button>
  )
);

所以 list 最终变成了 [7]。

当点击 2 时:

  • i 是模块级变量,值为 6
  • list 是闭包中的变量,值为 [0,1]

add 函数实际上执行的是:







请到「今天看啥」查看全文