React setState 异步的本质

367 阅读2分钟

一个例子:

import React from "react";

import "./styles.css";

export default class App extends React.Component{

  state = {

    count: 0

  }

  increment = () => {

    console.log('increment setState前的count', this.state.count)

    this.setState({

      count: this.state.count + 1

    });

    console.log('increment setState后的count', this.state.count)

  }

  triple = () => {

    console.log('triple setState前的count', this.state.count)

    this.setState({

      count: this.state.count + 1

    });

    this.setState({

      count: this.state.count + 1

    });

    this.setState({

      count: this.state.count + 1

    });

    console.log('triple setState后的count', this.state.count)

  }

  reduce = () => {

    setTimeout(() => {

      console.log('reduce setState前的count', this.state.count)

      this.setState({

        count: this.state.count - 1

      });

      console.log('reduce setState后的count', this.state.count)

    },0);

  }

  render(){

    return <div>

      <button onClick={this.increment}>点我增加</button>

      <button onClick={this.triple}>点我增加三倍</button>

      <button onClick={this.reduce}>点我减少</button>

    </div>

  }

}

一次点击 点我增加、点我增加三倍、点我减少,打印输出为:

    increment setState前的count 0
    increment setState后的count 0
    
    triple setState前的count 1
    triple setState后的count 1
    
    reduce setState前的count 2
    reduce setState后的count 1

前面的5个打印没有太多疑问:setState是异步的操作,但是最后一个,表现上是同步的操作

这里要从setState的批量更新说起: 每一次setState,都会触发一次update的生命周期:setState --> shouldComponentUpdate --> componentWillUpdate --> render --> componentDidUpdate

一个完整的更新流程,涉及了包括 re-render(重渲染) 在内的多个步骤。re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。假如说“一次 setState 就触发一个完整的更新流程”这个结论成立,那么每一次 setState 的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了

因此,这正是 setState 异步的一个重要的动机——避免频繁的 re-render

在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick 和浏览器里的 Event-Loop:每来一个 setState,就把它塞进一个队列里“攒起来”。等时机成熟,再把“攒起来”的 state 结果做合并,最后只针对最新的 state 值走一次更新流程。这个过程,叫作“批量更新”

实现上:React有一个全局的变量isBatchingUpdates,isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前会被修改为true,这时我们所做的 setState 操作自然不会立即生效,添加进队列中当函数执行完毕后,会再把 isBatchingUpdates 改为 false,而在setTimeout内部执行时,isBatchingUpdates已经被修改为了false所以造成了同步的假象。

对整个 setState 工作流做一个总结 setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。