一般在我们学习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就像是同步函数。
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(不一定准确,轻喷),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;