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 + 1 到 transactionWrappers.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} 赋值给ReactUpdates的batchingStrategy 的。然后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.isBatchingUpdates为false,这样会触发{a}分支来调用ReactDefaultBatchingStrategy.batchedUpdates() ;
2)batchedUpdates() 将ReactDefaultBatchingStrategy.isBatchingUpdates 设置为 true ,然后启动transaction ;
3)batchedUpdates 的 callback参数就是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
简单总结
今天先写到这。如果您觉得这篇不错,可以点赞或关注这个专栏。
感谢阅读!👋