React源码学习入门(十)setState是怎么做到异步化的?

170 阅读5分钟

React源码学习入门(十)setState是怎么做到异步化的?

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

本文基于React v15.6.2版本介绍,原因请参见新手如何学习React源码

源码解析

还记得我们之前在介绍React组件的时候,ReactComponent的实现吗?

让我们来回顾一下(源码位于src/isomorphic/modern/class/ReactBaseClasses.js):

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.isReactComponent = {};

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

可以看到React在setState入口也非常简单,并没有复杂的逻辑,只是调用了updaterenqueueSetState方法而已。

这个updater会在前面我们提到的挂载流程中注入,实际上是位于src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

enqueueSetState: function(publicInstance, partialState) {
  if (__DEV__) {
    ReactInstrumentation.debugTool.onSetState();
    warning(
      partialState != null,
      'setState(...): You passed an undefined or null state object; ' +
        'instead, use forceUpdate().',
    );
  }

  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );

  if (!internalInstance) {
    return;
  }

  var queue =
    internalInstance._pendingStateQueue ||
    (internalInstance._pendingStateQueue = []);
  queue.push(partialState);

  enqueueUpdate(internalInstance);
},

这段代码其实非常简单,就是通过当前React组件找到我们之前创建好的控制类实例,也就是代码里面的internalInstance,在它的_pendingStateQueue里面把当前要更新的state给push进去,然后调用enqueueUpdate方法。

enqueueUpdate位于src/renderers/shared/stack/reconciler/ReactUpdates.js中,这个方法则是整个React更新机制的灵魂:

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

这短短地几行代码里面蕴藏了整个更新机制的核心,就是这个isBatchingUpdates的控制变量,在之前挂载流程的时候我们有提过这个变量。

这里的逻辑其实比较简单,如果isBatchingUpdates是falsy,那直接调用batchingStrategy.batchedUpdates方法,否则就往dirtyComponents当中把当前的控制类实例push进去。

如果你直接去看这一块代码,可能很难理解这里面真正的含义是什么。

让我们回想一下,我们一般会把setState写在哪里。最常见的场景下,我们是在React生命周期的钩子函数中去调用setState,或者是在事件的回调函数里面。

而生命周期函数则是在React挂载和更新流程中触发,而在React挂载、事件触发前,我们的isBatchingUpdates已经开启了,回顾一下我们之前提到的挂载流程:

源码位于src/renderers/shared/stack/reconciler/ReactDefaultBatchingStategy.js:

batchedUpdates: function(callback, a, b, c, d, e) {
  var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

  ReactDefaultBatchingStrategy.isBatchingUpdates = true;

  // The code is written this way to avoid extra allocations
  if (alreadyBatchingUpdates) {
    return callback(ab, c, d, e);
  } else {
    return transaction.perform(callback, null, ab, c, d, e);
  }
},

这段代码我们之前分析过,对于首次进来的情况,会开启一个transaction

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

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

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

关注这两个Wrapper。它的执行顺序是先执行FLUSH_BATCHED_UPDATES,再执行RESET_BATCHED_UPDATES,而FLUSH_BATCHED_UPDATES这个Wrapper非常关键,它会在close回调的时候统一调用ReactUpdates.flushBatchedUpdates方法,然后再将isBatchingUpdates设为FALSE。

请注意,这个是在React挂载或是事件触发的时候启动的,它们是首次调用batchedUpdates的场景。

接着我们来看一下flushBatchedUpdates的实现:

var flushBatchedUpdates = function() {
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

这里面asap的逻辑我们可以不用管它,主要用于一些表单组件的事件触发当中。

这里的核心逻辑就是把我们之前的dirtyComponents中所有被标记的组件都取出来,依次执行runBatchedUpdates,当然这里开启了一个transaction,让我们来看看它的Wrapper:

var NESTED_UPDATES = {
  initialize: function() {
    this.dirtyComponentsLength = dirtyComponents.length;
  },
  close: function() {
    if (this.dirtyComponentsLength !== dirtyComponents.length) {
      dirtyComponents.splice(0, this.dirtyComponentsLength);
      flushBatchedUpdates();
    } else {
      dirtyComponents.length = 0;
    }
  },
};

var UPDATE_QUEUEING = {
  initialize: function() {
    this.callbackQueue.reset();
  },
  close: function() {
    this.callbackQueue.notifyAll();
  },
};

var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];

这里面UPDATE_QUEUEING很好理解,是用来处理setState的回调函数的。而NESTED_UPDATES,则是在一个setState更新的周期内,又遇到了嵌套的setState调用,这个比较常见在componentDidUpdate钩子当中,很可能update之后又触发了setState。这个Wrapper的核心作用是确保,当前setState更新结束之后,能够让嵌套的setState流程继续触发。

接着我们看看runBatchedUpdates的核心实现:

function runBatchedUpdates(transaction) {
  dirtyComponents.sort(mountOrderComparator);
  updateBatchNumber++;

  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];

    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance(),
        );
      }
    }
  }
}

runBatchedUpdates的逻辑,我们去除了一些干扰的分支逻辑,它的核心逻辑是非常清晰的,那就是依次将所有标记的dirtyComponent取出,分别执行performUpdateIfNecessary方法,这个也是React用来更新组件的核心方法。如果包含回调,则会在执行完成更新后,依次触发回调。

至此,其实setState整体的流程已经分析完了,可以看到这里利用了多个transaction和队列去做异步化,最后再通过performUpdateIfNecessary来真正做到更新,这就是batchedUpdate的核心原理。

小结一下

整个React的setState异步化,或者说是update流程的异步化,其实还是比较难以理解的,要结合我们之前讲解的transaction核心原理、React Mount挂载过程才可以比较好地理解到,整体异步化的原理我们用一幅图来总结一下:

最后我们思考一下React对更新做异步化的原因:

  • 出于性能考虑,update相对来说是一个比较重的操作,如果同步执行很多update,可能会导致浏览器出现卡顿,其实很多重复的setState操作都是可以合并成一次完成的。
  • 不打断当前的执行流程,比如我们本身是在做挂载的流程,正常来说挂载后面还有一些收尾工作要处理,如果这时候遇到了setState操作,这个流程就会被打断,从而直接进入了另一个更新流程,整个生命周期就会变得非常复杂,有些必要的回收和通知操作也无法执行了。

关于setState异步化的考虑gaearon已经在issue里回复的非常深刻了,具体可以参见这里

进一步交流讨论