setState同步还是异步

  • setState有时表现出异步,有时表现出同步,在合成事件钩子函数中表现异步
  • setState批量更新优化是建立在合成事件和钩子函数之上的,在原生事件和setTimeout中不会发生异步.(顺序为setState => 钩子函数 => 合并更新状态)

合成事件
  • React通过事件冒泡,将事件冒泡到document上面,再统一分发给中间层SyntheticEvent去执行事件
钩子函数
  • 钩子函数指生命周期的钩子

合成事件和钩子函数参考文档

setState之后到底发生了什么

setState有两个注意事项:
官网提供:

  1. 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用
  2. 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态
    1
    2
    3
    4
    5
    add() {
    this.setState({count:count + 1})
    this.setState({count:count + 1})
    }
    add()
  • 上述连续两次在点击事件调用setState

    图例

    执行顺序
    第一步
  • 首先调用实例的updater上的enqueueSetState
  • 目的是让setState入列
    1
    2
    3
    4
    5
    6
    7
    ReactComponent.prototype.setState = function (partialState, callback) {
    // 将setState事务放进队列中
    this.updater.enqueueSetState(this, partialState);
    if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
    }
    };
    第二步
  • 将更新状态的组件也放入队列中
  • 将新的state放进数组里
  • 用enqueueUpdate来处理将要更新的实例对象
目前达到的效果
  • setState在队列中
  • 在这个setState执行过程里 我们将state放入了一个队列 将要更新的组件也放入了一个队列
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     enqueueSetState: function (publicInstance, partialState) {

    // 获取当前组件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    // 将要更新的state放入一个数组里
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    // 将要更新的component instance也放在一个队列里
    enqueueUpdate(internalInstance);
    }
第三步
  • 调用enqueueUpdate执行更新操作,如果处在更新,那么只是将组件放入脏组件队列中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function enqueueUpdate(component) {
    // 如果没有处于批量创建/更新组件的阶段,则处理update state事务
    if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
    }
    // 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
    dirtyComponents.push(component);
    }
  • 这个更新函数,接收一个组件的实例 首先判断是否处于更新阶段 如果不是 那么执行更新 如果处在更新阶段 那么我将组件的实例push到队列中 等待更新
    这个函数也说明了setState是一个异步的过程,它会集齐一批需要更新的组件然后一起更新
第三步拓展——批量更新策略(batchingStrategy)
  • 第三步中,如果不处在更新状态,就执行batchingStrategy.batchedUpdates(enqueueUpdate,component)
批量更新策略所在的对象
  • batchedUpdates做了个判断,如果我处在更新状态,那么组件入列, 否则开启更新事务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var ReactDefaultBatchingStrategy = {
    // 用于标记当前是否出于批量更新
    isBatchingUpdates: false,
    // 当调用这个方法时,正式开始批量更新
    batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 如果当前事务正在更新过程中,则调用enqueueUpdate
    if (alreadyBatchingUpdates) {
    return callback(a, b, c, d, e);
    } else {
    // 否则执行更新事务
    return transaction.perform(callback, null, a, b, c, d, e);
    }
    }
    };
第三步拓展——事务
  • 所谓事务,就是把函数做一层包装,开始是做一些操作,结束时做一些操作

  • 下面是一个简单的事务例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    var Transaction = require('./Transaction');

    // 我们自己定义的 Transaction
    var MyTransaction = function() {
    // do sth.
    };

    Object.assign(MyTransaction.prototype, Transaction.Mixin, {
    getTransactionWrappers: function() {
    return [{
    initialize: function() {
    console.log('before method perform');
    },
    close: function() {
    console.log('after method perform');
    }
    }];
    };
    });

    var transaction = new MyTransaction();
    var testMethod = function() {
    console.log('test');
    }
    transaction.perform(testMethod);

    // before method perform
    // test
    // after method perform

  • React源码的事务中有两个事务RESET_BATCHED_UPDATES(用于重置更新状态) FLUSH_BATCHED_UPDATES(再发起一个dom的批量更新,包括渲染和虚拟dom比对等等)

  • FLUSH_BATCHED_UPDATES这个事务的目的在于循环所有更新组件,执行update,调用组件更新的生命周期等等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var RESET_BATCHED_UPDATES = {
    initialize: emptyFunction,
    close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
    }
    };

    var FLUSH_BATCHED_UPDATES = {
    initialize: emptyFunction,
    close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    };

总结

  • setState第一步:将state塞入数组,将组件交给更新队列
  • setState第二步:更新队列进行更新,如果更新队列正在更新,将组件塞入脏组件队列.否则执行批量更新策略
  • setState第三步:批量更新策略判断是否处在批量更新状态,如果正在更新,那么将更新组件塞入脏组件队列,否则把脏组件所有组件拿出来开启事务更新
  • setState第四步,批量更新结束事务,在事务结束前,会调用FLUSH_BATCHED_UPDATES计算最新的stateprops.然后关闭事务
  • 然后React可以拿到最新的state和props进行虚拟dom比对,虚拟dom比对完之后,会去渲染组件,此时如果有shouldUpdate,会进行一个是否渲染组件的判断

为什么setTimeout下setState是同步的

  • 关键在于批量更新策略什么时候开启的
  • React开启批量更新策略有两个位置,一个是钩子函数,一个是合成事件,可以将钩子函数和合成事件理解为一个大的事务,当触发时,React会开启批量更新策略.而当执行setTimeout的时候,大事务已经关闭,批量更新策略已经重置为false
  • 实际上,调用setState触发了两个批量更新事务,相当于同步更新的过程了.

面试问答

  • 问: setState过后到底发生了什么?
  • 答: 将组件交给更新队列,将state塞入数组,然后执行队列更新方法,如果队列正在更新,组件就塞入脏组件,否则执行批量更新策略, 批量更新策略会去判断是否正在批量更新,如果正在更新,会把队列放入脏组件,否则循环脏组件队列开启事务执行批量更新,然后在事务结束前计算最新state和props,React拿到他们去进行虚拟dom比对,组件渲染,判断shouldUpdate,进行更新.更新完执行componentDidUpdate…

React - setState源码分析(小白可读)
揭密React setState
react源码分析之-setState是异步还是同步?