setState是同步的还是异步的

Favori,

Setstate Usestate

图:Mako Tsereteli

“异步”?

所谓同步还是异步指的是调用 setState 之后是否马上能得到最新的 state

setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”

最终决定setState是同步渲染还是异步渲染的关键因素是ReactFiberWorkLoop工作空间的执行上下文.

什么时候是”异步”

legacy模式下:命中batchedUpdates时是异步 未命中batchedUpdates时是同步的

concurrent模式下:都是异步的

函数组件里使用useState都是异步的

//以下写法都会是”异步“效果
//所有打印的num都是0
export default () => {
  const [num, setNum] = useState(0);
  const handerClick = () => {
 
    setNum(num + 1);
    console.log(num);
    setNum(num + 1);
    console.log(num);
    setNum(num + 1);
    console.log(num);
 
    setTimeout(() => {
      setNum(num + 1);
      console.log(num);
      setNum(num + 1);
      console.log(num);
      setNum(num + 1);
      console.log(num);
    }, 1000);
 
    Promise.resolve().then(() => {
      setNum(num + 1);
      console.log(num);
      setNum(num + 1);
      console.log(num);
      setNum(num + 1);
      console.log(num);
    });
  };
 
  useLayoutEffect(() => {
    const dom = document.getElementById("button");
    if (!dom) return;
    dom.addEventListener("click", handerClick);
  }, []);
  
  return (
    <div>
      <button id="button">{num}</button>
    </div>
  );
};

什么时候是同步

  1. class组件原生事件触发的setState就是同步的
  2. class组件在setTimeout里是同步的
  3. class组件在promise.then里是同步的
// 以下表现就是同步的
// 每一步会打印不同的值
export default class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
  }
  handerClick = () => {
    setTimeout(() => {
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
    }, 1000);
    Promise.resolve().then(() => {
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
      this.setState({ number: this.state.number + 1 });
      console.log(this.state.number);
    });
  };
 
  render() {
    return (
      <div>
        <button onClick={this.handerClick}>num++</button>
      </div>
    );
  }
}
 

如何手动合并多次更新

ReactDOM.unstable_batchedUpdates

为什么?

  1. 合成事件会走batchUpdate
  2. 在 setState 的时候react内部会创建一个 updateQueue ,通过 firstUpdate 、 lastUpdate 、 lastUpdate.next 去维护一个更新的队列,在最终的 performWork 中,相同的key会被覆盖,只会对最后一次的 setState 进行更新
  3. 在批量更新上下文中(比如点击事件对应的处理函数),this.setState 会创建一个update,这个更新对应一个 expirationTime,Sync mode下(默认情况)它的值是1。将这个 update 添加到 该组件对应fiber 的 updateQueue。同时将这个过期时间添加到RootFiber。由于处于批量更新上下文中,这些update不会被执行。 然后继续this.setState,再次创建一个 expirationTime 为 1 的update,将这个 update 添加到 fiber 的 updateQueue…如此反复。 直到退出事件处理函数,然后将批量更新的标志(isBatchingUpdates)设置为false,此时 performSyncWork 执行所有的update。

Tips

批量更新:是一个组件连续 setState 多次,这个组件只更新一次,这与过期时间无关 批处理:同一时间,这个组件应该并发更新多少次,这个和过期时间有关,或者说和 lane 有关