• setState函数的异步性
    • 简述
      • 详解
      • 解决setState函数异步的办法?
        • 例子
        • 更多关于setState的小知识
    • 参考资料:

    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算法。

    参考资料:

    • https://medium.com/@wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3#.jhdhncws3
    • https://www.bennadel.com/blog/2893-setstate-state-mutation-operation-may-be-synchronous-in-reactjs.htm