深入理解React源码 - 界面更新 VII(正式版)

1,149 阅读5分钟

Photo by Felix Mooneeram on Unsplash

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

上次我们聊完了Transaction核心类和它的一个实例ReactDefaultBatchingStrategyTransaction。然而这个Transaction 实例仅仅是一个开始 。

本篇中,我们将探究其它的Transaction 实例。看懂了这些,界面更新逻辑的轮廓就画的出来了。

本文用到的文件:

renderers/shared/stack/reconciler/ReactUpdates.js: 定义了本文的入口函数,flushBatchedUpdates() ;它也定义了ReactUpdatesFlushTransaction,本文会重点关注的其中一个Transaction 实例

shared/utils/PooledClass.js: 定义了PooledClass ,这个类被用于开启其它类的实例池功能

renderers/dom/client/ReactReconcileTransaction.js: 定义了ReactReconcileTransaction ,另外一个重点关注的Transaction

我们从上次讨论到的函数 ReactUpdates.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) { // scr: not applied
...
    }
  }
};
...

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

之前提到过,这个函数会启动 ReactUpdatesFlushTransaction来处理所有的dirtyComponents ,而这个即将启动的Transaction链会最终调用runBatchedUpdates 来完成本文将会讨论的整个操作。

`PooledClass` - 实例池

上面一段代码里面有两个函数不太常见,getPooled()release()。这两个函数是从PooledClass 继承而来,用于实现实例池:

a)如果从来没有分配过(ReactUpdatesFlushTransaction)实例,getPooled()创建一个新实例;

b)如果池里已经有实例了,getPooled() 直接从池里返回已经分配好的实例;

c)release() 并不是真的释放掉内存,而是将实例放回池子里。

和其他的各种“池”一样,这个实例池的目的也是减少不必要的资源(这里是内存)创建,和回收开销。

回到我们刚刚的例子:

...
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
...

ReactUpdatesFlushTransaction 实例只会在第一次进入while 循环时被创建。接下来的访问会直接从池里捞。

关于PooledClass 知道这些就完全不影响接下来的理解了,所以想跳过此小节的同学请ctrl-f “ReactUpdatesFlushTransaction”。你随意,我继续写

我们来看它的具体实现:

var PooledClass = {
  addPoolingTo: addPoolingTo,                     // scr: ------> 1)
  oneArgumentPooler: (oneArgumentPooler: Pooler), // scr: ------> 2)
...// scr: not used
};
module.exports = PooledClass;

PooledClass@shared/utils/PooledClass.js

1)addPoolingTo() 是(事实上的)“public”函数,它用来给既存的类注入实例池的功能;

2)oneArgumentPooler() 则是getPooled() 的底层实现。

下面我们看下addPoolingTo() 的函数体:

...
var DEFAULT_POOL_SIZE = 10;
var DEFAULT_POOLER = oneArgumentPooler;
...

var addPoolingTo = function<T>(
  CopyConstructor: Class<T>,
  pooler: Pooler,
): Class<T> & {
  getPooled(): /* arguments of the constructor */ T,
  release(): void,
} {
  var NewKlass = (CopyConstructor: any);
  NewKlass.instancePool = [];              // scr: -------------> 1)
  NewKlass.getPooled = pooler || DEFAULT_POOLER; // scr: -------> 2)
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE; // scr: -------------> 3)
  }
  NewKlass.release = standardReleaser;     // scr: -------------> 4)
  return NewKlass;
};
...

addPoolingTo@shared/utils/PooledClass.js

1)instancePool 就是上面说的实例池;

2) 将getPooled() 设置成 DEFAULT_POOLER(a.k.a., oneArgumentPooler);

3)将poolSize 赋值为10

4)将release() 设置成standardReleaser()

然后是getPooled()release() 的实现:

var oneArgumentPooler = function(copyFieldsFrom) {
  var Klass = this;
  if (Klass.instancePool.length) {           // scr: -----------> 1)
    var instance = Klass.instancePool.pop(); // scr: -----------> 1)
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    return new Klass(copyFieldsFrom);   // scr: ----------------> 2)
  }
};

oneArgumentPooler@shared/utils/PooledClass.js

...

var standardReleaser = function(instance) {
  var Klass = this;
...
  instance.destructor();
  if (Klass.instancePool.length < Klass.poolSize) { // scr: ----> 3)
    Klass.instancePool.push(instance);              // scr: ----> 3)
  }
};

standardReleaser@shared/utils/PooledClass.js 

1)对应 b),这里面

Klass.call(instance, copyFieldsFrom);

使用copyFieldsFrom 作为参数调用了Klass 的构造函数;

2)对应a);

3)对应c)。

最后,我们看一下addPoolingTo() 是如何被外面调用的:

...
PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
...

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

ReactUpdatesFlushTransaction

...
function ReactUpdatesFlushTransaction() {
  this.reinitializeTransaction();
  this.dirtyComponentsLength = null;
  this.callbackQueue = CallbackQueue.getPooled();
  this.reconcileTransaction =
  ReactUpdates.ReactReconcileTransaction.getPooled(  // scr: ---> 2)
    /* useCreateElement */ true,
  );
}

// scr: --------------------------------------------------------> 1)
Object.assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS; // scr: -----------------------> 3)
  },

  destructor: function() {
    this.dirtyComponentsLength = null;
    CallbackQueue.release(this.callbackQueue);
    this.callbackQueue = null;
    ReactUpdates.ReactReconcileTransaction.release( // scr: ----> 2)
      this.reconcileTransaction
    );

    this.reconcileTransaction = null;
  },

  perform: function(method, scope, a) {
    return Transaction.perform.call(
      this,
      this.reconcileTransaction.perform, // scr: ---------------> 2)
      this.reconcileTransaction,
      method,
      scope,
      a,
    );
  },
});
...

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

1)这是另外一个Transaction 的实例,并且覆写了perform() 方法;

2)这个类并没有直接调用传给它的callback参数 (ReactUpdate.runBatchedUpdates),而是在这个覆写过的ReactUpdatesFlushTransaction.perform() 中嵌套调用 另一个Transaction(ReactReconcileTransaction) 的perform() 函数,再把这个callback参数(i.e., ReactUpdate.runBatchedUpdates())透传下去。这里注意ReactReconcileTransaction 也开启了实例池;

3)TRANSACTION_WRAPPERS 定义了它的前置和后置函数:

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

var UPDATE_QUEUEING = { // scr: ------> we omit this wrapper for now
  initialize: function() {
    this.callbackQueue.reset();
  },
  close: function() {
    this.callbackQueue.notifyAll();
  },
};

var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
...

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

这里NESTED_UPDATESinitialize() 1.5)缓存了dirtyComponents的数量;然后在close() 中3)检查这个数量有没有变。如果变了{a}flushBatchedUpdates() 会被调用然后重新递归整个过程;或者{b}没变的话,它会将dirtyComponents.length 设置为0 ,然后返回至上层TransactionReactDefaultBatchingStrategyTransaction上一篇}的上下文。

对于(UPDATE_QUEUEING 里的)CallbackQueue 相关的操作我就不展开讨论了。留给以后的讨论组件生命周期文章再说。*9

简单总结下:

ReactReconcileTransaction

这仅仅是另一个普通的Transaction

var Mixin = {
  /**
   * @see Transaction
   * @abstract
   * @final
   * @return {array<object>} List of operation wrap procedures.
   *   TODO: convert to array<TransactionWrapper>
   */
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS; // scr: -----------------------> 1)
  },

  // scr: ---------------------> we omit all CallbackQueue s for now
  getReactMountReady: function() {
    return this.reactMountReady;
  },

  // scr: ---------------------> we omit all CallbackQueue s for now
  getUpdateQueue: function() {
    return ReactUpdateQueue;
  },

  checkpoint: function() { // scr: -----------------------> not used
    // reactMountReady is the our only stateful wrapper
    return this.reactMountReady.checkpoint();
  },

  rollback: function(checkpoint) { // scr: ---------------> not used
    this.reactMountReady.rollback(checkpoint);
  },

  // scr: ------------------------------------> for instance pooling
  destructor: function() {
    CallbackQueue.release(this.reactMountReady);
    this.reactMountReady = null;
  },
};

Object.assign(ReactReconcileTransaction.prototype, Transaction, Mixin);

// scr: --------------------------------------------------------> 2)
PooledClass.addPoolingTo(ReactReconcileTransaction);

module.exports = ReactReconcileTransaction;

ReactReconcileTransaction@renderers/dom/client/ReactReconcileTransaction.js

1)它的包装类定义在TRANSACTION_WRAPPERS 中;

2)之前提到过,它有实例池。

下面我们一把看完它的三个包装类:

/**
 * Ensures that, when possible, the selection range (currently selected text
 * input) is not disturbed by performing the transaction.
 */
var SELECTION_RESTORATION = {
  /**
   * @return {Selection} Selection information.
   */
  initialize: ReactInputSelection.getSelectionInformation,
  /**
   * @param {Selection} sel Selection information returned from `initialize`.
   */
  close: ReactInputSelection.restoreSelection,
};

/**
 * Suppresses events (blur/focus) that could be inadvertently dispatched due to
 * high level DOM manipulations (like temporarily removing a text input from the
 * DOM).
 */
var EVENT_SUPPRESSION = {
  /**
   * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before
   * the reconciliation.
   */
  initialize: function() {
    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
    ReactBrowserEventEmitter.setEnabled(false);
    return currentlyEnabled;
  },

  /**
   * @param {boolean} previouslyEnabled Enabled status of
   *   `ReactBrowserEventEmitter` before the reconciliation occurred. `close`
   *   restores the previous value.
   */
  close: function(previouslyEnabled) {
    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
  },
};

/**
 * Provides a queue for collecting `componentDidMount` and
 * `componentDidUpdate` callbacks during the transaction.
 */
var ON_DOM_READY_QUEUEING = {
  /**
   * Initializes the internal `onDOMReady` queue.
   */
  initialize: function() {
    this.reactMountReady.reset();
  },

  /**
   * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
   */
  close: function() {
    this.reactMountReady.notifyAll();
  },
};

var TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,
  EVENT_SUPPRESSION,
  ON_DOM_READY_QUEUEING,
];

ReactReconcileTransaction@renderers/dom/client/ReactReconcileTransaction.js

其实注释已经写的很清楚了:

SELECTION_RESTORATION 用来在界面更新操作之前存储文本框的选择状态(initialize()),更新操作完了就恢复回来(close());

EVENT_SUPPRESSION 用来在界面操作的时候静默(浏览器)事件,并将之前的“事件是否关闭的状态”缓存(initialize()),就是说不管之前有没有关,这里会把它暂时关闭;然后在更新操作结束后恢复之前缓存的状态。

ON_DOM_READY_QUEUEING里的)CallbackQueue 也不讨论了,留给以后的生命周期。*10

这里要注意,ReactReconcileTransaction 是依赖于原始的Transaction.perform(),其对应的回调就是从本文开始被透传到这一步的ReactUpdate.runBatchedUpdates

可以用ctrl-f “runBatchedUpdates”查看一下它的透传轨迹。

这个ReactUpdate.runBatchedUpdates 会引出下一篇的内容。

简单总结下:

总结

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

感谢阅读!👋