前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一道有挑战性的 React Hook 场景题,考考你的功底

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

作者头像
Leecason
发布2022-07-13 14:30:18
5720
发布2022-07-13 14:30:18
举报
文章被收录于专栏:小李的前端小屋

本文分享一个短小而又深刻的 React Hook 场景题,这个例子涉及到:

  • hook 闭包问题
  • state 更新机制

希望看完以后你会对 React 函数组件有更深入的了解。

场景复现

整个 Demo 非常简单,大家可以自己在电脑上尝试一下。

首先,有一个 button 和一个 list:

代码语言:javascript
复制
<div className="App">
  <button onClick={add}>Add</button>
  {list.map(val => val)}
</div>

list 是使用 useState 管理的状态。button 绑定了事件 onClick={add}

点击按钮,会执行 add 方法向 list 中加入一些内容。

代码语言:javascript
复制
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

代码语言:javascript
复制
let i = 0;

export default function App() {
  // ...
}

接着重点来看看 add 方法。

调用 add,会向 list 中添加新的 button,新 button 也绑定了 onClick={add}

代码语言:javascript
复制
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 函数:

代码语言:javascript
复制
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 计算出来的:

代码语言:javascript
复制
current state = base state + update1 + update2 + …

每次会将上一次的 prev state 与 update 进行合并得到新的 current state。

因此,每次调用 useState 返回的 list 都不是同一个对象,它们的内存地址不同。

这会导致每个「数字按钮」的 add 函数处于不同的闭包中,每个闭包当中的 list 都不同。

而变量 i 是声明在 App 外层的模块级变量,在每个闭包中 i 都是相同的。

代码语言:javascript
复制
let i = 0;

export default function App() {
  // ...
}

所以,在点击 0 时:

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

add 函数实际上执行的是:

代码语言:javascript
复制
setList(
  [].concat(
    <button key={7} onClick={add}>{7}</button>
  )
);

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

当点击 2 时:

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

add 函数实际上执行的是:

代码语言:javascript
复制
setList(
  [0, 1].concat(
    <button key={7} onClick={add}>{7}</button>
  )
);

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

为了方便理解,这里的 [0, 1, 7] 省略了外层的 <button> 标签

如何解决

那么如何解决这个闭包问题,在 list 后面正常拼接 button 呢?

很简单,只要将 list 从闭包中清理出去就可以了,将 setList 参数改为函数形式

之前是:

代码语言:javascript
复制
setList(
  list.concat(
    <button 
      key={i} 
      onClick={add}>
      {i++}
    </button>
  )
);

修改为:

代码语言:javascript
复制
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 的更新机制是:

代码语言:javascript
复制
current state = base state + update1 + update2 + …

所以每次调用 useState,返回值 list 都是不同的对象。

并且由于闭包的存在,每个「数字按钮」add 函数中的 list 都不同。

两者共同作用,造成了这种不符合直觉的现象。

如何解决这种闭包问题呢?我们可以将 setState 改为函数形式,将变量从闭包中清理出去。

参考

  • https://betterprogramming.pub/a-react-hooks-challenge-for-senior-react-developers-f1190e1939ec
  • https://github.com/facebook/react/blob/a8c9cb18b7/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js

谢谢支持❤️

如果觉得有用,就点赞在看分享吧,谢谢大家啦~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小李的前端小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景复现
  • 问题
  • 解答
  • 原理剖析
  • 如何解决
  • 总结
  • 参考
  • 谢谢支持❤️
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档