setState函数的异步性

简述

在某些情况下,React框架出于性能优化考虑,可能会将多次state更新合并成一次更新。正因为如此,setState实际上是一个异步的函数。
但是,有一些行为也会阻止React框架本身对于多次state更新的合并,从而让state的更新变得同步化。
比如: eventListeners, Ajax, setTimeout 等等。

详解

当setState() 函数执行的时候,函数会创建一个暂态的state作为过渡state,而不是立即修改this.state。
如果在调用setState()函数之后尝试去访问this.state,你得到的可能还是setState()函数执行之前的结果。
在使用setState()的情况下,看起来同步执行的代码其实执行顺序是得不到保证的。原因上面也提到过,React可能会将多次state更新合并成一次更新来优化性能。

运行下面这段代码,你会发现当和addEventListener, setTimeout 函数或者发出AJAX call的时候,调用setState, state会发生改变。并且render函数会在setState()函数被触发之后马上被调用。那么到底发生了什么呢?事实上,类似setTimeout()函数或者发出ajax call的fetch函数属于调用浏览器层面的API,这些函数的执行并不存在与React的上下文中,所以React并不能够像控制其他存在与其上下文中的函数一样,将多次state更新合并成一次。

在上面这些例子中,React框架之所以在选择在调用setState函数之后立即更新state而不是采用框架默认的方式,即合并多次state更新为一次更新,是因为这些函数调用(fetch,setTimeout等浏览器层面的API调用)并不处于React框架的上下文中,React没有办法对其进行控制。React在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state)。

  1. class TestComponent extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. dollars: 10
  6. }
  7. this.onMouseLeaveHandler = this.onMouseLeaveHandler.bind(this);
  8. this.onTimeoutHandler = this.onTimeoutHandler.bind(this);
  9. this.onAjaxCallback = this.onAjaxCallback.bind(this);
  10. this.onClickHandler = this.onClickHandler.bind(this);
  11. }
  12. componentDidMount() {
  13. // Add custom event via `addEventListener`
  14. //
  15. // The list of supported React events does include `mouseleave`
  16. // via `onMouseLeave` prop
  17. //
  18. // However, we are not adding the event the `React way` - this will have
  19. // effects on how state mutates
  20. //
  21. // Check the list here - https://facebook.github.io/react/docs/events.html
  22. document.getElementById('testButton').addEventListener('mouseleave', this.onMouseLeaveHandler);
  23. // Add JS timeout
  24. //
  25. // Again,outside React `world` - this will also have effects on how state
  26. // mutates
  27. setTimeout(this.onTimeoutHandler, 10000);
  28. // Make AJAX request
  29. fetch('https://api.github.com/users')
  30. .then(this.onAjaxCallback);
  31. }
  32. onClickHandler = () => {
  33. console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
  34. this.setState({
  35. dollars: this.state.dollars + 10
  36. });
  37. console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
  38. }
  39. onMouseLeaveHandler = () => {
  40. console.log('State before (mouseleave): ' + JSON.stringify(this.state));
  41. this.setState({
  42. dollars: this.state.dollars + 20
  43. });
  44. console.log('State after (mouseleave): ' + JSON.stringify(this.state));
  45. }
  46. onTimeoutHandler = () => {
  47. console.log('State before (timeout): ' + JSON.stringify(this.state));
  48. this.setState({
  49. dollars: this.state.dollars + 30
  50. });
  51. console.log('State after (timeout): ' + JSON.stringify(this.state));
  52. }
  53. onAjaxCallback = (err, res) => {
  54. if (err) {
  55. console.log('Error in AJAX call: ' + JSON.stringify(err));
  56. return;
  57. }
  58. console.log('State before (AJAX call): ' + JSON.stringify(this.state));
  59. this.setState({
  60. dollars: this.state.dollars + 40
  61. });
  62. console.log('State after (AJAX call): ' + JSON.stringify(this.state));
  63. }
  64. render() {
  65. console.log('State in render: ' + JSON.stringify(this.state));
  66. return (
  67. <button
  68. id="testButton"
  69. onClick={this.onClickHandler}>
  70. 'Click me'
  71. </button>
  72. );
  73. }
  74. }
  75. ReactDOM.render(
  76. <TestComponent />,
  77. document.getElementById('app')
  78. );

解决setState函数异步的办法?

根据React官方文档,setState函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState函数执行后的回调。通过传入回调函数的方式,React可以保证传入的回调函数一定是在setState成功更新this.state之后再执行。

例子

  1. _onClickHandler: function _onClickHandler() {
  2. console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
  3. this.setState({
  4. dollars: this.state.dollars + 10
  5. }, () => {
  6. console.log('Here state will always be updated to latest version!');
  7. console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
  8. });
  9. }

更多关于setState的小知识

其实setState作为一个函数,本身是同步的。只是因为在setState的内部实现中,使用了React updater的enqueueState 或者 enqueueCallback方法,才造成了异步。

下面这段是React源码中setState的实现:

  1. ReactComponent.prototype.setState = function(partialState, callback) {
  2. invariant(
  3. typeof partialState === 'object' ||
  4. typeof partialState === 'function' ||
  5. partialState == null,
  6. 'setState(...): takes an object of state variables to update or a ' +
  7. 'function which returns an object of state variables.'
  8. );
  9. this.updater.enqueueSetState(this, partialState);
  10. if (callback) {
  11. this.updater.enqueueCallback(this, callback, 'setState');
  12. }
  13. };

而updater的这两个方法,又和React底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOM的diff算法。

参考资料: