react源码解剖——setState的异步模型

在我的另一篇文章 凭什么说virtual DOM是React的精髓所在 中提到过,react的性能优化,要归功于批量DOM处理和Diff算法。关于Diff算法的文章,各平台一抓一大把,有兴趣的同学可以自行查阅。今天,我们就React的批量DOM处理,从一个小例子聊起,来探索一下setState 之后,究竟发生了什么。

抛砖引玉

我们先来看一个简单的例子,十秒钟思考然后确定控制台弹出了什么:

var Test = React.createClass({
  getInitialState: function(){
    return {value: 'Hello Mangee'}
  },
  handleChangeValue: function(){
    this.setState({
      value: 'Goodbye Mangee'
    });
    console.log('newValue is', this.state.value);
  },
  render: function(){
    return <button onClick={this.handleChangeValue}>changeValue</button>;
  }
})
ReactDOM.render(
  <Test />,
  document.getElementById('example')
);

看完这个例子,大多数人都会自然而然认为控制台弹出了 “newValue is Goodbye Mangee”,但事与愿违,控制台实际上是弹出了原先的值—— “newValue is Hello Mangee”。

这是为什么呢?我们来看看官方对于 setState 的一段注解:

Sets a subset of the state. Always use this to mutate
state. You should treat this.state as immutable.

There is no guarantee that this.state will be immediately updated, so
accessing this.state after calling this method may return the old value.

There is no guarantee that calls to setState will run synchronously,
as they may eventually be batched together. You can provide an optional
callback that will be executed when the call to setState is actually
completed.

从这段话中可以得知,setState对state的更新是异步的,原因正是为了实现我们的文首提及的批量DOM处理。

于是我们可以得到这样一条信息:依靠 setState 的异步性,React在一段时间间隔内,将所有DOM更新收集起来,然后批量处理。因此,学习 setState 的异步模型,也有助于你对 React 性能优化策略的进一步了解。

异步模型解剖

由于 React 源码使用了大量的继承和依赖注入,部分对象的方法需要依据依赖或继承关系一层层追溯,这里我不做逐步分析,想要深入了解的同学可以自行研究。

那么接下来,就跟随笔者的脚步,通过源码来探寻一下从 setState 到 state 改变的完整过程。

在此之前,你需要准备好 React 的两个包,React 和 ReactDOM。

npm install react
npm install react-dom
从 setState 说起
```   
this.setState({});
```   

当执行到 setState 时,我们需要来找找 setState 是在哪定义的,以此来探寻 setState 后的第一步。

// react/ReactComponent
ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

我们发现,setState 调用了组件本身的 updater 对象的两个方法enqueueSetState 和 enqueueCallback,其中,callback 是更新完成后执行的回调函数。

updater(更新器),每个 React 组件都拥有的、用于驱动 state 更新的工具对象,按照继承依赖关系,可以追溯到 updater 的本体,即react-dom/ReactUpdateQueue,其中定义了我们所要找的 enqueueSetState 和 enqueueCallback 两个方法。

那么择其一,enqueueSetState 里边,又发生了什么?

// react-dom/ReactUpdateQueue
enqueueSetState: function (publicInstance, partialState) {

  // 获得 internalInstance  实例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

  // 将 partialState 推入实例自身的 _pendingStateQueue (状态队列)等候更新
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);

  // 驱动更新
  enqueueUpdate(internalInstance);
}

enqueueCallback 的实现步骤跟以上一样,最终结果也是将回调函数 callback 推入回调队列,等待执行。

到目前,待更新的 state 已经在状态队列里候着了,什么时候拿出来更新呢?这就得来继续看看 enqueueUpdate 这个函数了。

// react-dom/ReactUpdateQueue
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

原来是执行了 ReactUpdates 模块的 enqueueUpdate 方法,让我们把频道切换到 react-dom/ReactUpdates。

// react-dom/ReactUpdates
function enqueueUpdate(component) {

  // 若 batchingStrategy 不处于批量更新收集状态,则 batchedUpdates 所有队列中的更新
  // 值得注意的是,此时传入的 component 将不参加当前批的更新,而是作为下一批进行更新
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // 若 batchingStrategy 处于批量更新收集状态,则把 component 推进 dirtyComponents 数组等待更新
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

这里,batchingStrategy 对象是作为一个批处理的管理者,依照指定的批量策略,对到来的一系列 component 更新做分批。

设想一个场景:我们去玩过山车时,管理员会分批安排游客进场,等这一批游客玩完之后,再安排下一批进场,而在当前批游客正在玩的过程中,有游客到来,都需要先排队。

在这里,component 就是游客,batchingStrategy 就是管理员,isBatchingUpdates 标志就是有没有游客正在玩。当一批DOM处理完成后,调用 batchedUpdates 方法,更新下一批 dirtyComponents。

有些人可能会有疑问,为什么这里感觉像是开了两个线程,一个在完成“排队”,一个在完成“批处理”。实际上不是的,js是单线程的,所以当一个event loop内陆陆续续有新的 component 更新驱动来到这里时,都会被阻塞在 dirtyComponents 中,等到全部收集完毕,才进行批处理,不存在边处理边排队的情况。

另外,值得注意的是,batchingStrategy 对象是通过 injection 方法注入的,经过一番艰难追溯之后,发现了 batchingStrategy 就是 ReactDefaultBatchingStrategy。让我们看看这个模块调用 batchedUpdates 方法之后,发生了什么。

// react-dom/ReactDefaultBatchingStrategy 
var ReactDefaultBatchingStrategy = {

  isBatchingUpdates: false,

  batchedUpdates: function (callback, a, b, c, d, e) {

    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

可以看出,ReactDefaultBatchingStrategy 对象十分简洁,isBatchingUpdates 是批收集判断的标志位,batchedUpdates 方法用于发动一个批处理。在其中我们可以发现,isBatchingUpdates 标志位就是在 batchedUpdates 发起的时候置为 true 的。那 isBatchingUpdates 又是在哪里复位为 false 的呢?这就得引出一个React 框架设计的核心概念——Transaction (事务)。

随处可见的Transaction

Transaction(事务)是一个针对函数执行的包装(wrapper),React关于 Transaction 的源码中,出现了这样一幅有趣而形象的图:

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

从上图可知,Transaction 实例 transaction 在创建的时候向自身注入 wrapper,实现的效果是,通过 transaction.perform 执行的函数 anyMethod,先执行 transaction 的所有 initialize 方法,再执行 anyMethod,执行完再执行所有的 close 方法。引用来自 React 源码剖析系列 - 解密 setState 的一个简单的例子说明:

// react-dom/Transaction 
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

基于此,我们回过头来看看,ReactDefaultBatchingStrategy.batchedUpdates 执行后,发生了什么。

// react-dom/ReactDefaultBatchingStrategy 
var ReactDefaultBatchingStrategy = {

  isBatchingUpdates: false,

  batchedUpdates: function (callback, a, b, c, d, e) {

    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

batchedUpdates 方法中,transaction 是 ReactDefaultBatchingStrategyTransaction 的实例,也是一类事务,perform 方法传入的 callback 正是我们前边探究过的、用于做DOM批收集的 enqueueUpdate 函数。现在让我们把注意力转移到它的 initialize 和 close 方法上:

// react-dom/ReactDefaultBatchingStrategy 
// 定义复位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定义批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

initialize 方法是两个空函数,我们不关注,close 方法,按照顺序,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新。到这里我们恍然大悟,所谓的批处理,实际上是明确地分为了批收集和批更新两个步骤,而上边所有的内容,都只是在完成批收集这个环节。

React 对于这个核心环节的设计可是一点都不含糊,所以懵逼了的同学请翻回去重新来一遍,还没吐的同学请坚持。

批更新

整理一下妆容,我们继续来看看这后续的批更新环节是如何实现的。

对于批更新这部分,涉及到关于 React 从 virtual DOM 向真实 DOM 反馈的许多细节考虑,一来笔者未能完全渗透,二来与本文所要探究的问题无关,因此接下来贴出的源码是经过大量删减的,只保留了我们需要关注的部分。

衔接批收集的最后一步,ReactUpdates 模块调用了 flushBatchedUpdates 方法。

// react-dom/ReactUpdates 
var flushBatchedUpdates = function () {

  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      // 创建一个 ReactUpdatesFlushTransaction 实例
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // 调用实例的 perform 方法进行更新
      transaction.perform(runBatchedUpdates, null, transaction);
      // 释放实例,回归实例池
      ReactUpdatesFlushTransaction.release(transaction);
    }
  }
};

核心步骤又出现了另外一个 transaction,它执行了一个 runBatchedUpdates 函数。当然,老规矩,遇到 transaction,查查它的 initialize 和 close 方法是很必要的,但由于 runBatchedUpdates 执行的调用栈比较深入,要讲的略多,所以我们放到 runBatchedUpdates 执行完毕再来看。先关注 runBatchedUpdates 完成了哪些:

// react-dom/ReactUpdates 
function runBatchedUpdates(transaction) {

  var len = transaction.dirtyComponentsLength;

  // 排序,保证 dirtyComponent 从父级到子级的 render 顺序
  dirtyComponents.sort(mountOrderComparator);
  updateBatchNumber++;

  // 遍历 dirtyComponents
  for (var i = 0; i < len; i++) {

    var component = dirtyComponents[i];

    // 取到该 dirtyComponent 的回调数组
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    // 更新该 dirtyComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);

    // 当存在 callbacks 时,将 callbacks 逐项提取,推入 transaction.callbackQueue 
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

遍历 dirtyComponents 数组,并且利用一个新模块 ReactReconciler 的
performUpdateIfNecessary 方法将 dirtyComponent 逐个更新。

让我们来看看 ReactReconciler.performUpdateIfNecessary 完成了什么:

// react-dom/ReactReconciler
performUpdateIfNecessary: function (internalInstance, transaction, updateBatchNumber) {
  internalInstance.performUpdateIfNecessary(transaction);
}

调用了组件的 performUpdateIfNecessary 方法,而又一番艰苦追溯,我们发现,组件为 ReactCompositeComponent 的实例,因而也在 ReactCompositeComponent 中发现了关于它的定义:

// react-dom/ReactReconciler
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {

  var inst = this._instance;
  var nextContext = inst.context;
  var nextProps = nextParentElement.props;

  ``` // 对 comtext 和 props 的一系列校验

  // 关注的核心
  var nextState = this._processPendingState(nextProps, nextContext);

  ``` // 拿到更新后的 nextState 进行反馈到真实 DOM 上的更新
}

最终,整个过程算是绕了一圈,调用了组件上的 _processPendingState 方法,在这个方法中,我们终于完成了对 state 的合并更新:

_processPendingState: function (props, context) {
  var inst = this._instance;
  var queue = this._pendingStateQueue;
  var replace = this._pendingReplaceState;
  this._pendingReplaceState = false;
  this._pendingStateQueue = null;

  if (!queue) {
    return inst.state;
  }

  if (replace && queue.length === 1) {
    return queue[0];
  }

  var nextState = _assign({}, replace ? queue[0] : inst.state);
  // 将该组件状态队列里所有的 state 更新统一处理合并
  for (var i = replace ? 1 : 0; i < queue.length; i++) {
    var partial = queue[i];
    _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
  }

  return nextState;
}

咦,说好的回调函数会在更新完成后调用的呢?
别急,不是还漏了前文提到的那个 transaction 的 close 方法没瞧瞧嘛:

var NESTED_UPDATES = {
  initialize: function () {
    this.dirtyComponentsLength = dirtyComponents.length;
  },
  close: function () {
    // 移除已遍历过的 dirtyComponents
    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 () {
    // 完成更新后执行 callbackQueue 的回调函数
    this.callbackQueue.notifyAll();
  }
};

var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];

function ReactUpdatesFlushTransaction() {
}

_assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
}

看,配合得真完美,不出所料,正是利用了 transaction 的 close 方法,将一开始缓存在 callbacks 队列中的回调函数,逐一取出并执行,这里我就不做展开了。

捋一捋思路

经过了这样一系列复杂而深入的调用,setState 终于完成了 state 的合并更新。但其实,我所提取的只是 setState 的一个通用过程,文首抛出的例子,其实早在 click 事件触发的那一刻起,就已经执行了一个 batchedUpdates,因此等执行到 setState 的时候,已经置身于一个大的 transaction 中,其调用栈已经非常深入了。但是篇幅限制,也因笔者能力有限,故而放弃对 react 完整的事件触发机制进行深入探讨,这里就大致地还原一下setState的异步流机制,给看到这里还没崩溃的同学,总结一下吧:

1、click事件触发;
2、React 内置事件监听器启动一个事务(transaction) ,把批策略(ReactDefaultBatchingStrategy)的批收集标志位置为 true;
3、在事务的 perform 中,setState发起;
4、触发更新器(updater)上的 enqueueSetState 和 enqueueCallback,把 state 和 callback 推入等待队列,并且驱动 enqueueUpdate 更新;
5、触发 batchingStrategy 的 batchedUpdates 方法,启动一个事务,进行批收集;
6、收集完成后,触发事务的 close 方法,复位标志位,并执行批处理;
7、触发 ReactUpdates 的 flushBatchedUpdates 方法,启动另外一个事务,执行一系列的调用最终完成更新;
8、更新完成后,触发事务的 close 方法,调用队列里的回调函数;
9、最外层的事务完成,释放调用栈。

关于 setState 的异步模型解析就到这里,学艺不精,恐有错漏,欢迎吐槽!

参考文献如下,极力推荐:
拆解setState[一][一源看世界][之React]
拆解setState[二][一源看世界][之React]
拆解setState[三][一源看世界][之React]
setState 之后发生了什么 —— 浅谈 React 中的 Transaction
React 源码剖析系列 - 解密 setState
React源码分析5 — setState机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容