React setState 既是同步也是异步

一般在我们学习React的时候,大家都会牢记的一点是 setState 是异步的,想要拿到最新的state值有两种方法(可以先到最后看一遍完整的代码,再回来看):

1.在后面的回调函数拿到最新的state

reactClick = () => {
    const { cnt } = this.state
    this.setState({ cnt: cnt + 1 }, () => {
      console.log('after reactClick setState cnt is', this.state.cnt);
      console.log('reactClick end -----------------------');
    })
    console.log('reactClick now cnt is', cnt);
  }

2.利用 async 和 await 拿到最新的state。(其实这里是把异步变成同步了,所以我还是把这情况归为异步情况)

asyncClick = async () => {
    let res = await this.setState({ cnt: this.state.cnt + 1 }, () => {
      console.log('after asyncClick setState cnt is', this.state.cnt);
    })
    console.log('asyncClick', res); // res 是 undefined (由于好奇想看看res是什么,至于为什么是undefined,要去看react setState 的源码)
    console.log('after asyncClick cnt is', this.state.cnt);
    console.log('asyncClick end -----------------------');
  }

 

下面介绍 React setState 是同步的两个情况:

1. 把setState 绑定在原生的js事件上

jsClick = () => {
    const { cnt } = this.state
    console.log('before jsClick cnt is', this.state.cnt);
    this.setState({ cnt: cnt + 1 }, () => {
      console.log('after jsClick setState cnt is', this.state.cnt);
    })
    console.log('after jsClick cnt is', this.state.cnt);
    console.log('jsClick end -----------------------');
  }

这里需要结合 jsClick 和 reactClick 的代码,然后看红线分割出来代码的输出顺序。

先看reactClick的输出顺序,这里的输出顺序应该没有太大问题,可以看到先是输出 reactClick now cnt is 1 ,然后执行 render (在render里面输出了 cnt),最后再是执行 setState的回调函数 。从这里,我们可以看到 setState之后,我们并没有拿到最新的 cnt,而是在回调函数中拿到了,所以 setState 在这种情况下(react的合成事件,例如:onClick、onBlur 等,react 自己重写的 dom事件)是百分百的异步函数。

这里我们需要记住一点,先执行render ,然后在执行 setState 的回调函数(注意是setState的回调函数)。

 接着我们重点来看 jsClick的输出循序 :

1. before jsClick cnt is 0

在点击了按钮的原生事件时,此时还没有执行 setState ,先输出  before jsClick cnt is 0 。 

2. render cnt is 1 (react事件)

然后就是执行了 setState, 触发了 render ,输出 render cnt is 1

3. after jsClick setState cnt is 1 (react事件)

接着先是执行了 setState 的回调函数,输出 after jsClick setState cnt is 1

4. after jsClick cnt is 1 (原生js事件)

最后再是  after jsClick cnt is 1

 

从上面的输出顺序可以看出,react的 render setState 以及setState的回调函数,都是比原生js早执行的。那么就不难解释为什么setState在原生事件上是同步的。

原生的事件是在react事件完成之后执行的(下面要用到),需要补充的一点是,这里我是把 setState 之后执行的代码理解为原生js事件。那么我们在 setState 之后去拿到的state,就是最新的state,所以看起来setState就像是同步函数。

React setState 既是同步也是异步

2.setState 在 setTimeout里面

timeOut = () => {
    console.log('before setTimeout cnt is',this.state.cnt);
    setTimeout(() => {
      this.setState({ cnt: this.state.cnt + 1 }, () => {
        console.log('after setTimeout setState cnt is', this.state.cnt);
      })
      console.log('after timeOut', this.state.cnt)
      console.log('setTimeout end -----------------------');

    }, 0)
  }

setTimeout的输出循序:

1.  before setTimeout cnt is 0 

这里的第一个输出应该没什么问题,开启了一个setTimeout的异步任务,先执行同步的任务,等同步任务结束之后,再去执行异步任务。

2. render cnt is 1 (react事件)

然后执行 setState ,触发了render 输出 render cnt is 1 

3. after setTimeout setState cnt is 1 (react事件)

接着执行了 setState 的回调函数,输出 after setTimeout setState cnt is 1

4. after timeOut 1 (原生js事件)

最后再是执行原生js,输出 after timeOut 1

 

我们可以利用 jsClick 那里得出的结论:原生的事件是在react事件完成之后执行的,来理解输出循序。其中输出顺序2和输出顺序3是react事件,而输出循序4和setTimeout是js原生事件。那么在setTimeout中执行setState,就好比在原生事件中执行setState。而react的执行机制是先执行react事件,然后在执行js原生事件。

React setState 既是同步也是异步

 

 

 

 结论:

通过上面的例子,推测出react(不一定准确,轻喷),react遇到自己的事件(setState,onClick),先执行自己的事件。自己的事件全部执行完了之后(包括setState等一些自己的回调函数),才去执行原生js事件。造成这样的原因可能是虚拟dom引起的,因为我们知道使用react操作页面上的元素其实操作的都是虚拟dom。

react渲染页面的时候,都是根据虚拟dom去生成真实的dom,那么react的合成事件只是写在虚拟dom上的,而不是真实的dom。那么在操作虚拟dom时,我们只能遵守react的执行机制(同步还是异步等),所以在生成真实的dom时,虚拟dom肯定是更新完成了。

真实dom根据虚拟dom来展示自己,那么肯定先要完成虚拟dom的更新,由此推出react事件肯定是执行完成了。那我们再去执行的js原生事件,一定是在react事件之后的。

以下是样例的完整代码:

import React, { Component } from 'react';

class App extends Component {
  state = {
    cnt: 0,
  }
  componentDidMount() {
    document.getElementById('JSBtn').addEventListener('click', this.jsClick)
  }
  jsClick = () => {
    const { cnt } = this.state
    console.log('before jsClick cnt is', this.state.cnt);
    this.setState({ cnt: cnt + 1 }, () => {
      console.log('after jsClick setState cnt is', this.state.cnt);
    })
    console.log('after jsClick cnt is', this.state.cnt);
    console.log('jsClick end -----------------------');
  }
  reactClick = () => {
    const { cnt } = this.state
    this.setState({ cnt: cnt + 1 }, () => {
      console.log('after reactClick setState cnt is', this.state.cnt);
      console.log('reactClick end -----------------------');
    })
    console.log('reactClick now cnt is', cnt);
  }
  timeOut = () => {
    console.log('before setTimeout cnt is',this.state.cnt);
    setTimeout(() => {
      this.setState({ cnt: this.state.cnt + 1 }, () => {
        console.log('after setTimeout setState cnt is', this.state.cnt);
      })
      console.log('after timeOut', this.state.cnt)
      console.log('setTimeout end -----------------------');

    }, 0)
  }
  asyncClick = async () => {
    let res = await this.setState({ cnt: this.state.cnt + 1 }, () => {
      console.log('after asyncClick setState cnt is', this.state.cnt);
    })
    console.log('asyncClick', res); // res 是 undefined (由于好奇想看看res是什么,至于为什么是undefined,要去看react setState 的源码)
    console.log('after asyncClick cnt is', this.state.cnt);
    console.log('asyncClick end -----------------------');
  }
  render() {
    console.log(`${this.state.cnt === 0 ? 'this is first(init) render' : 'render'} cnt is`, this.state.cnt);
    return (
      <div>
        <button id='JSBtn'>JSBtn</button>
        <button onClick={this.reactClick}>ReactBtn</button>
        <button onClick={this.timeOut}>setTimeout</button>
        <button onClick={this.asyncClick}>asyncClick</button>
      </div>
    );
  }
}

export default App;

 

上一篇:SetState


下一篇:react进阶第三讲——state