经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
深入剖析setState同步异步机制
来源:cnblogs  作者:陌上兮月  时间:2021/1/18 16:25:44  对本文有异议

关于 setState

setState 的更新是同步还是异步,一直是人们津津乐道的话题。不过,实际上如果我们需要用到更新后的状态值,并不需要强依赖其同步/异步更新机制。在类组件中,我们可以通过this.setState的第二参数、componentDidMountcomponentDidUpdate等手段来取得更新后的值;而在函数式组件中,则可以通过useEffect来获取更新后的状态。所以这个问题,其实有点无聊。

不过,既然大家都这么乐于讨论,今天我们就系统地梳理一下这个问题,主要分为两方面来说:

  • 类组件(class-component)的更新机制
  • 函数式组件(function-component)的更新机制

类组件中的 this.setState

在类组件中,这个问题的答案是多样的,首先抛第一个结论:

  • legacy模式中,更新可能为同步,也可能为异步;
  • concurrent模式中,一定是异步。

问题一、legacy 模式和 concurrent 模式是什么鬼?

  • 通过ReactDOM.render(<App />, rootNode)方式创建应用,则为 legacy 模式,这也是create-react-app目前采用的默认模式;

  • 通过ReactDOM.unstable_createRoot(rootNode).render(<App />)方式创建的应用,则为concurrent模式,这个模式目前只是一个实验阶段的产物,还不成熟。

legacy 模式下可能同步,也可能异步?

是的,这不是玄学,我们来先抛出结论,再来逐步解释它。

  1. 当直接调用时this.setState时,为异步更新;
  2. 当在异步函数的回调中调用this.setState,则为同步更新;
  3. 当放在自定义 DOM 事件的处理函数中时,也是同步更新。

实验代码如下:

  1. class StateDemo extends React.Component {
  2. constructor(props) {
  3. super(props)
  4. this.state = {
  5. count: 0
  6. }
  7. }
  8. render() {
  9. return <div>
  10. <p>{this.state.count}</p>
  11. <button onClick={this.increase}>累加</button>
  12. </div>
  13. }
  14. increase = () => {
  15. this.setState({
  16. count: this.state.count + 1
  17. })
  18. // 异步的,拿不到最新值
  19. console.log('count', this.state.count)
  20. // setTimeout 中 setState 是同步的
  21. setTimeout(() => {
  22. this.setState({
  23. count: this.state.count + 1
  24. })
  25. // 同步的,可以拿到
  26. console.log('count in setTimeout', this.state.count)
  27. }, 0)
  28. }
  29. bodyClickHandler = () => {
  30. this.setState({
  31. count: this.state.count + 1
  32. })
  33. // 可以取到最新值
  34. console.log('count in body event', this.state.count)
  35. }
  36. componentDidMount() {
  37. // 自己定义的 DOM 事件,setState 是同步的
  38. document.body.addEventListener('click', this.bodyClickHandler)
  39. }
  40. componentWillUnmount() {
  41. // 及时销毁自定义 DOM 事件
  42. document.body.removeEventListener('click', this.bodyClickHandler)
  43. }
  44. }

要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。

首先我们来看看 setState 的主流程:

  1. 调用this.setState(newState)
  2. newState会存入 pending 队列;
    3,判断是不是batchUpdate
    4,如果是batchUpdate,则将组件先保存在所谓的脏组件dirtyComponents中;如果不是batchUpdate,那么就遍历所有的脏组件,并更新它们。

由此我们可以判定:所谓的异步更新,都命中了batchUpdate,先保存在脏组件中就完事;而同步更新,总是会去更新所有的脏组件。

非常有意思,看来是否命中batchUpdate是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?

这就涉及到一个很有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些我们看不到的东西,现在我们通过魔法让其显现出来:

  1. increase = () => {
  2. // 开始:默认处于bashUpdate
  3. // isBatchingUpdates = true
  4. this.setState({
  5. count: this.state.count + 1
  6. })
  7. console.log('count', this.state.count)
  8. // 结束
  9. // isBatchingUpdates = false
  10. }
  1. increase = () => {
  2. // 开始:默认处于bashUpdate
  3. // isBatchingUpdates = true
  4. setTimeout(() => {
  5. // 此时isBatchingUpdates已经设置为了false
  6. this.setState({
  7. count: this.state.count + 1
  8. })
  9. console.log('count in setTimeout', this.state.count)
  10. }, 0)
  11. // 结束
  12. // isBatchingUpdates = false
  13. }

当 react 执行我们所书写的函数时,会默认在首位设置isBatchingUpdates变量。看到其中的差异了吗?当 setTimeout 执行其回调时,isBatchingUpdates早已经在同步代码的末尾被置为false了,所以没命中batchUpdate

那自定义 DOM 事件又是怎么回事?代码依然如下:

  1. componentDidMount() {
  2. // 开始:默认处于bashUpdate
  3. // isBatchingUpdates = true
  4. document.body.addEventListener("click", () => {
  5. // 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了
  6. this.setState({
  7. count: this.state.count + 1,
  8. });
  9. console.log("count in body event", this.state.count); // 可以取到最新值。
  10. });
  11. // 结束
  12. // isBatchingUpdates = false
  13. }

我们可以看到,当componentDidMount跑完时,isBatchingUpdates已经设置为false了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates当然也是false,不会命中batchUpdate机制。

总结:

  • this.setState是同步还是异步,关键就是看能否命中batchUpdate机制
  • 能不能命中,就是看isBatchingUpdatestrue还是false
  • 能命中batchUpdate的场景包括:生命周期和其调用函数、React中注册的事件和其调用函数。总之,是React可以“管理”的入口,关键是“入口”。

这里要注意一点:React去加isBatchingUpdate的行为不是针对“函数”,而是针对“入口”。比如setTimeout、setInterval、自定义DOM事件的回调等,这些都是React“管不到”的入口,所以不会去其首尾设置isBatchingUpdates变量。

concurrent 模式一定是异步更新

因为这个东西只在实验阶段,所以要开启 concurrent 模式,同样需要将 react 升级为实验版本,安装如下依赖:

  1. npm install react@experimental react-dom@experimental

其他代码不用变,只更改 index 文件如下:

  1. - ReactDOM.render(<App />, document.getElementById('root'));
  2. + ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);

则可以发现:其更新都是异步的,在任何情况下都是如此。

关于函数式组件中 useState 的 setter

在函数式组件中,我们会这样定义状态:

  1. const [count, setCount] = useState(0)

这时候,我们发现当我们无论在同步函数还是在异步回调中调用 setCount 时,打印出来的 count 都是旧值,这时候我们会说:setCount 是异步的。

  1. const [count, setCount] = useState(0);
  2. // 直接调用
  3. const handleStrightUpdate = () => {
  4. setCount(1);
  5. console.log(count); // 0
  6. };
  7. // 放在setTimeout回调中
  8. const handleSetTimeoutUpdate = () => {
  9. setTimeout(() => {
  10. setCount(1);
  11. console.log(count); // 0
  12. });
  13. };

setCount 是异步的,这确实没错,但是产生上述现象的原因不只是异步更新这么简单。原因主要有以下两点:

1,调用 setCount 时,会做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders),从这个角度上来说,setCount确实是一个异步操作;

2,函数式的capture-value特性决定了console.log(count)语句打印的始终是一个只存在于当前帧的常量,所以就算无论 setCount 是不是同步的,这里都会打印出旧值。

原文链接:http://www.cnblogs.com/zhangnan35/p/14290542.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号