深入React中setState的同步异步

开发中遇到的问题

在一个下拉加载列表的需求中,有以下的代码:
在这里插入图片描述
其中,onScroll 是页面滚动事件的回调函数,throttle 是一个节流方法。获取列表的方法在 useEffect 中条件判断后执行,而 currentPage、isBottom、needLoad 都是依赖数组的一部分,它们的状态值改变时,都可能重新获取列表。

最开始 setCurrentPage(currentPage + 1) 是写在 setNeedLoad(true) 后面的,然后出现了一个 bug,就是会用原本的 currentPage 去请求列表,列表数据就出现了问题。

研究了以后发现,其实是因为 throttle 是一个异步执行的方法,而 setState 在其中是同步的,所以此时页面渲染了 3 次,每个状态值改变都执行了一次 useEffect。而在 currentPage 状态值改变前,就通过了条件判断重新获取了列表,所以出现了这个问题。把 setCurrentPage(currentPage + 1) 移到最前面之后,解决了这个问题。

下面就深入说一下 setState 的同步异步。

class 和 hook 中 setState 的同步异步

在 class 和 hook 中 setState 的同步异步表现是一致的。

在生命周期和 React 注册的事件中,setState 是异步的,只渲染一次;在定时器等异步任务和自定义 DOM 事件中,setState 是同步的,每执行一次 setState 都会重新渲染。

class

异步

import React from 'react';
import { Button } from 'antd';

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log('count: ', this.state.count);

    this.setState({ count: this.state.count + 1 });
    console.log('count: ', this.state.count);
  }

  render() {
    const { count } = this.state;
    console.log('render count: ', count);

    return (
      <div>
        <Button onClick={this.handleClick}>{count}</Button>
      </div>
    );
  };
}

export default Demo;

点击按钮后的打印结果:
在这里插入图片描述

  • 在生命周期和 React 注册的事件中,setState 是异步的,在 handleClick 中取值 count 时还没有更新,所以最终 count 只加了 1
  • 由于 setState 是异步的,所以只执行了一次 render

同步

...
  
handleClick = () => {
  setTimeout(() => {
    console.log('count: ', this.state.count);
    this.setState({ count: this.state.count + 1 });
    console.log('count: ', this.state.count);
    this.setState({ count: this.state.count + 1 });
    console.log('count: ', this.state.count);
  });
}

...

点击按钮后的打印结果:
在这里插入图片描述

  • 在自定义 DOM 事件和定时器等异步任务中,setState 是同步的,在 handleClick 中取值 count 时已经更新,所以最终 count 加了 2
  • 由于 setState 是同步的,所以每一次 setState 都会执行一次 render

hook

异步

import React, { useState } from 'react';
import { Button } from 'antd';

const Demo = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log('count: ', count);

    setCount(count + 1);
    console.log('count: ', count);
  };

  console.log('demo count: ', count);
  
  return (
    <div>
      <Button onClick={handleClick}>{count}</Button>
    </div>
  );
}

点击按钮后的打印结果:
在这里插入图片描述

  • 此时,和 class 的情况一样,setCount 是异步的,在 handleClick 中取值 count 时还没有更新,所以最终 count 只加了 1
  • 由于 setCount 是异步的,所以只重新渲染了一次

同步

1)setCount 的参数是状态值

...

const handleClick = () => {
  setTimeout(() => {
    console.log('count: ', count);
    setCount(count + 1);
    console.log('count: ', count);
    setCount(count + 1);
    console.log('count: ', count);
  });
};

...

点击按钮后的打印结果:
在这里插入图片描述

2)setCount 的参数是回调函数

...

const handleClick = () => {
  setTimeout(() => {
    console.log('count: ', count);
    setCount(value => value + 1);
    console.log('count: ', count);
    setCount(value => value + 1);
    console.log('count: ', count);
  });
};

...

点击按钮后的打印结果:
在这里插入图片描述

3)总结

  • 此时,setCount 是同步的,但是和 class 的情况不太一样,因为 setTimeout 是个闭包,所以在 handleClick 中取值 count 是更新前的值
  • 当 setCount 的参数是状态值时,因为 count 的取值一直是 0,相当于执行了两次 setCount(1),所以最终 count 只加了 1;当参数是回调函数时,因为 value 是最新的状态值,所以最终 count 加了 2
  • 由于是 setCount 是同步的,所以每次 setCount 都重新渲染了一次

setState 的同步异步取决于是否命中 batchUpdate 机制

setState 是通过一个队列机制实现的 state 更新,执行 setState 的时候,会把需要更新的 state 合并后放入状态队列,不会立刻更新 this.state。

setState 的简化调用栈为:
在这里插入图片描述
dirtyComponents 为现在 state 已经被更新了的 components。

判断是否处于批量更新模式(即是否命中 batchUpdate 机制)的标识就是 isBatchingUpdates 的值。当 isBatchingUpdates 为 true 时表示处于批量更新模式,state 会被暂存进队列中,模拟出异步的效果;当 isBatchingUpdates 为 false 时,会直接更新 state。

isBatchingUpdates 的值默认为 false,值的改变和事务 transaction 有关。事务开始前 isBatchingUpdates 会置为 true,事务结束后 isBatchingUpdates 会置为 false。

结合上面的例子:

1)

...

handleClick = () => {
  // 开始:处于批量更新模式
  // isBatchingUpdates 为 true
  this.setState({ count: this.state.count + 1 });
  console.log('count: ', this.state.count); // count: 0
  // 结束
  // isBatchingUpdates 为 false
}

...

2)

...

handleClick = () => {
  // 开始:处于批量更新模式
  // isBatchingUpdates 为 true
  setTimeout(() => {
    // 执行时 isBatchingUpdates 已经置为了 false
    this.setState({ count: this.state.count + 1 });
    console.log('count: ', this.state.count); // count: 1
  });
  // 结束
  // isBatchingUpdates 为 false
}

...

所以有了 setState 同步异步的不同情况。

能命中 batchUpdate 机制(即 setState 异步)的情况有:生命周期、React 中注册的事件,总之就是 React 可以“管理”的入口。

不能命中 batchUpdate 机制(即 setState 同步)的情况有:定时器等异步任务、自定义 DOM 事件,总之就是 React “管不到” 的入口。

Legacy 模式、Blocking 模式、Concurrent 模式中 setState 的同步异步情况

  • Legacy 模式: ReactDOM.render(<App />, rootNode)。这是当前 React app 使用的方式,当前没有计划删除本模式。
  • Blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中,作为迁移到 Concurrent 模式的第一个步骤。
  • Concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />)。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。

在 Legacy 模式中,setState 可能为同步,也可能为异步。使用 ReactDOM.unstable_batchedUpdates 能强制批量更新(不建议)。

在 Blocking 模式和 Concurrent 模式中,setState 一定为异步。

参考

深入剖析setState同步异步机制(没有找到原文出处)
setState异步、同步与进阶
React官网 - Concurrent 模式(实验性)
《深入REACT技术栈》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值