深入理解React源码 - 界面更新 VI

深入理解React源码 - 界面更新 VI

SemlohSemloh

Photo by Jacob Ufkes on Unsplash

本文也同时发表在我的博客HACKERNOON

从某种程度来说,优雅,高效的界面更新是React的核心竞争力。在深入理解React里用来加速界面更新的各种奇技淫巧(比如virtual DOM和Diffing算法)之前,我们需要先了解上述技巧的打开方式,Transaction

本篇涉及的文件:

renderers/shared/utils/Transaction.js: Transaction 核心类定义在这里

renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js: ReactDefaultBatchingStrategyTransaction 和它的 API 包装类 ReactDefaultBatchingStrategy 定义在这里

renderers/shared/stack/reconciler/ReactUpdates.js: 这里的 enqueueUpdate() 作为主要路径用到了 ReactDefaultBatchingStrategy

之前的文章一般是从常用API入手,而本篇则会用自底向上的方式来解读这段逻辑。

那我们先来看看

Transaction 这个核心类

这个类的实际public函数是perform , 它也提供了这个类的主要功能:

...
/**
...
   *
   * @param {function} method Member of scope to call.
   * @param {Object} scope Scope to invoke from.
   * @param {Object?=} a Argument to pass to the method.
   * @param {Object?=} b Argument to pass to the method.
   * @param {Object?=} c Argument to pass to the method.
   * @param {Object?=} d Argument to pass to the method.
   * @param {Object?=} e Argument to pass to the method.
   * @param {Object?=} f Argument to pass to the method.
   *
   * @return {*} Return value from `method`.
   */
  perform: function<
    A,
    B,
    C,
    D,
    E,
    F,
    G,
    T: (a: A, b: B, c: C, d: D, e: E, f: F) => G,
  >(method: T, scope: any, a: A, b: B, c: C, d: D, e: E, f: F): G {
    /* eslint-enable space-before-function-paren */
...
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
...
      // one of these calls threw.
      errorThrown = true;
      this.initializeAll(0);
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
...
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
...
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },
...

TransactionImpl@renderers/shared/utils/Transaction.js

除了调用 callback (这个函数的第一个参数)以外,perform() 还做了两件事 1)在调 callback 之前调用 initializeAll() 和 2)之后调用closeAll()

这里errorThrown 用来标记调用method.call() 的时候是否发生异常。(如果抛异常了errorThrown 不会被设置回false, 并且逻辑会直接走到finally

下面我们看一下上述的前置和后置函数,

...
  initializeAll: function(startIndex: number): void {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
...
        this.wrapperInitData[i] = OBSERVED_ERROR;
        this.wrapperInitData[i] = wrapper.initialize
          ? wrapper.initialize.call(this)
          : null;
      } finally {
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

...
  closeAll: function(startIndex: number): void {
...// scr: sanity check
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== OBSERVED_ERROR && wrapper.close) {
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  },
};

export type Transaction = typeof TransactionImpl;

TransactionImpl@renderers/shared/utils/Transaction.js

很直白了,这两个函数会遍历this.transactionWrappers 然后分别调用它们对应的initialize()close() 方法。

this.transactionWrappers 则是在Transaction 的事实上的构造函数里面,用this.getTransactionWrappers() 来初始化的:

...
  reinitializeTransaction: function(): void {
    this.transactionWrappers = this.getTransactionWrappers();
    if (this.wrapperInitData) {
      this.wrapperInitData.length = 0;
    } else {
      this.wrapperInitData = [];
    }
    this._isInTransaction = false;
  },
...

TransactionImpl@renderers/shared/utils/Transaction.js

我们马上会看到这个this.transactionWrappers 具体是啥了。

这里的异常处理比较有趣。拿initializeAll() 举例。当initialize() 里有异常抛出时,它的finally 代码块(而不是catch)会调用完余下的this.transactionWrappers (i.e., 下标从 i + 1transactionWrappers.length-1)的initialize() 函数。然后异常会打断for 循环和整个initializeAll() 函数调用,而直接走到perform()finally 中。这样,就在初始化有异常的情况下绕开了

ret = method.call(scope, a, b, c, d, e, f);

调用。最后closeAll() 会在同一个finally 代码块中关闭事物流。

现在我们明白了Transaction 的核心是啥,但是具体用来做什么呢?下一节我们用一个Transaction 的实例来回答这个问题。

ReactDefaultBatchingStrategyTransaction

首先,ReactDefaultBatchingStrategyTransaction 是一个Transaction 的子类,并且实现了getTransactionWrappers()

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

ReactDefaultBatchingStrategyTransaction@renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js 

其次,TRANSACTION_WRAPPERS 就是this.transactionWrappers 的实体了。正是它们提供了perform() 用到的前置函数(initialize())和后置函数(close())。

...
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
}; // scr: -----------------------------------------------------> 2)

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}; // scr: -----------------------------------------------------> 2)

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];  // scr: -------------------------------> 2)

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
} // scr: ------------------------------------------------------> 1)
...
  // scr: ------------------------------------------------------> 3)
var transaction = new ReactDefaultBatchingStrategyTransaction();
...

ReactDefaultBatchingStrategyTransaction@renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

1)ReactDefaultBatchingStrategyTransaction 的构造函数调用了基类Transaction 的构造函数,然后用第2)步定义的FLUSH_BATCHED_UPDATES 初始化this.transactionWrappers

2)定义了两个包装类和它们对应的initialize()close() ,这些函数会被分别用在Transaction.initializeAll(),和Transaction.closeAll()

3)将ReactDefaultBatchingStrategyTransaction 定义成一个单例。

最后我们看一下ReactDefaultBatchingStrategy 里面的public 函数

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  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) { // scr: --------> not applied here
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

ReactDefaultBatchingStrategy@renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

ReactDefaultBatchingStrategy 是通过注入机制 {第二篇 *5} 赋值给ReactUpdatesbatchingStrategy 的。然后ReactDefaultBatchingStrategy.batchedUpdates() 则被用在 ReactUpdates.enqueueUpdate() 里。这个 函数也是setState() 的底层实现。

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) { // scr: ----------> {a}
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // scr: -----------------------------------------------------> {b}
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    // scr: this field is used for sanity check later
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

ReactUpdates@renderers/shared/stack/reconciler/ReactUpdates.js

这里的递归风格有点像{上篇}介绍过的内循环。

1)当第一次进入这个函数时ReactDefaultBatchingStrategy.isBatchingUpdatesfalse,这样会触发{a}分支来调用ReactDefaultBatchingStrategy.batchedUpdates()

2)batchedUpdates()ReactDefaultBatchingStrategy.isBatchingUpdates 设置为 true ,然后启动transaction

3)batchedUpdatescallback参数就是enqueueUpdate() 本身,所以 enqueueUpdate() 会立即被transaction.perform 再次调用。 注意这里两个wrapper的前置函数(initialize())都是emptyFunction 所以两次调用enqueueUpdate() 之间并不会发生任何事情;

4)当第二次进入enqueueUpdate() (在刚刚启动的transaction 上下文中),{b}分支会被触发

...
dirtyComponents.push(component);
... 

5)enqueueUpdate()返回后,FLUSH_BATCHED_UPDATES的后置函数(close()) 会被调用。这些后置函数会处理上一步中标记为dirtyComponents 的组件。

*8 我们会在下篇讨论这个FLUSH_BATCHED_UPDATES.close()ReactUpdates.flushBatchedUpdates() 函数体。

6)RESET_BATCHED_UPDATES 的后置函数会被调用,然后将ReactDefaultBatchingStrategy.isBatchingUpdates 设置成false。至此整个流程完成。

这里很重要的一点是,在3)到6)之间的后续enqueueUpdate() 调用都会在ReactDefaultBatchingStrategy.isBatchingUpdates:false 的上下文中执行,这意味着这些调用都会走{b}分支。

->dirtyComponents.push(component);
->dirtyComponents.push(component);
->dirtyComponents.push(component);
...
----->ReactUpdates.flushBatchedUpdates

简单总结

今天先写到这。如果您觉得这篇不错,可以点赞或关注这个专栏。

感谢阅读!👋

文章被以下专栏收录
3 条评论