React ReactUpdates

简述

ReactUpdates模块在整个生命周期中是基础性质的模块,它是调用虚拟DOM算法更新DOM的主要调用者,其重要性不言而喻。

React在更新DOM过程中有几个重要的职责,其中事务职责和批量更新职责是核心。

这其中事务负责实用innerHTML更新小范围DOM时候,将其中各种状态预先保存,完成替换后还原这些状态。批量更新则包括componentDidMount带来的更新,以及用户操作带来的各种更新。

结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ReactUpdates = {
/**
* React references `ReactReconcileTransaction` using this property in order
* to allow dependency injection.
*
* @internal
*/
ReactReconcileTransaction: null,

batchedUpdates: batchedUpdates,
enqueueUpdate: enqueueUpdate,
flushBatchedUpdates: flushBatchedUpdates,
injection: ReactUpdatesInjection,
asap: asap
};
属性/方法作用
ReactReconcileTransaction更新需要依赖的事务调度
batchedUpdates批量更新方法
enqueueUpdate队列更新方法
flushBatchedUpdatesbatchedUpdates的实际执行者
injection依赖注入,ReactReconcileTransaction、batchingStrategy(batchedUpdates依赖)由它来注入
asap将回调排入队列以在当前批处理循环结束时运行

分析

enqueueUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enqueueUpdate(component) {
ensureInjected();

// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)

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

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

这个函数是一个入队操作。

如果当前没有处于更新过程中,那么直接进行更新(batchingStrategy.batchedUpdates(enqueueUpdate, component))不加入dirtyComponents

否则将需要更新的component压入到dirtyComponents,并设置component._updateBatchNumber

batchedUpdates

本质上只是对batchingStrategy.batchedUpdates(callback, a, b, c, d, e)的调用。

batchingStrategy对应的路径是:src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

所以这里重点就扯一扯batchingStrategy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
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);
}
},
};

这里更新逻辑还是比较简单,根据维护的isBatchingUpdates这个标记来判定直接回调调用还是事务完成后进行函数调用。这里核心还是ReactDefaultBatchingStrategyTransaction。关于这个事务定义比较简单是RESET_BATCHED_UPDATES && FLUSH_BATCHED_UPDATES两个事务的组合。

核心是事务完成后执行:

1
2
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)

这里就涉及到flushBatchedUpdates了,而asap被它依赖,所以先扯一扯它。

asap

1
asapCallbackQueue.enqueue(callback, context);

这个函数核心代码只有一行。asapCallbackQueue.enqueue作用是将一个函数和其上下文推入到维护的函数队列中。

关于这个函数,它是一个特殊的操作,它是针对bug #1698的解决方案。

它的调用主要位于表单元素Input、Select和TextArea的_handleChange函数中,主要调用是ReactUpdates.asap(forceUpdateIfMounted, this);,用于在事务close外添加一个给CallbackQueue队列进行入队的操作,以图实现最快进行更新。

flushBatchedUpdates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var flushBatchedUpdates = function() {
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
// updates enqueued by setState callbacks and asap calls.
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);
}
}
};

其一 对象池

这里先关注dirtyComponents这一截。

这里对dirtyComponents进行了遍历,如果没有对象池处理,每次遍历过程中的更新都会重新new一个ReactUpdatesFlushTransaction事务,然后重新销毁。但是这里有了事务之后就可以直接getPooled(),用完之后可以使用release缓存,这样对象池里面就是一个初始化状态的实例。

当然提到release这个过程显然跑不了查看一下实例上的destructor——transaction.release环节会执行这个函数。它实质上是调用instance.destructor()后将instance压入到对象池方便后面getPooled获取。

1
2
3
4
5
6
7
destructor: function() {
this.dirtyComponentsLength = null;
CallbackQueue.release(this.callbackQueue);
this.callbackQueue = null;
ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
this.reconcileTransaction = null;
},

对照一下其构造器:

1
2
3
4
5
6
7
8
function ReactUpdatesFlushTransaction() {
this.reinitializeTransaction();
this.dirtyComponentsLength = null;
this.callbackQueue = CallbackQueue.getPooled();
this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled(
/* useCreateElement */ true,
);
}

很容易发现这两个操作除了dirtyComponentsLength赋值外都是反向操作。很有意思的是他们里面也有一个CallbackQueue的getPooled和release过程。这里需要主要就是对象池的操作了,release执行时候会执行instance.destructor使实例进入初始化环境放到对象池,getPooled获取的时候回重新执行一次构造器。

除此以外就是runBatchedUpdates函数的执行了。

其二 ReactUpdatesFlushTransaction事务

ReactUpdatesFlushTransaction事务是NESTED_UPDATESUPDATE_QUEUEING的组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var NESTED_UPDATES = {
initialize: function() {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function() {
if (this.dirtyComponentsLength !== dirtyComponents.length) {
// Additional updates were enqueued by componentDidUpdate handlers or
// similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
// these new updates so that if A's componentDidUpdate calls setState on
// B, B will update before the callback A's updater provided when calling
// setState.
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0;
}
},
};

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

UPDATE_QUEUEING比较简单,初始化时候清空队列中的回调和context,事务结束时候则按上下文调用所有回调。

NESTED_UPDATES相对复杂一点。初始化时候缓存dirtyComponents.lengthdirtyComponentsLength,事务结束时候检查缓存的长度值和当前dirtyComponents.length是否相等,相等则清空dirtyComponents,否则在dirtyComponents里面移除已经更新好的,重新执行flushBatchedUpdates来继续更新后续加入的组件,这是一个递归操作直到dirtyComponents.length === 0。这个一般发生在父组件更新导致子组件联动更新上。

runBatchedUpdates

这个函数没有导出使用,但是在内部则实质上承担了调用组件的更新逻辑职责。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
invariant(
len === dirtyComponents.length,
"Expected flush transaction's stored dirty-components length (%s) to " +
'match dirty-components array length (%s).',
len,
dirtyComponents.length,
);

// Since reconciling a component higher in the owner hierarchy usually (not
// always -- see shouldComponentUpdate()) will reconcile children, reconcile
// them before their children by sorting the array.
dirtyComponents.sort(mountOrderComparator);

// Any updates enqueued while reconciling must be performed after this entire
// batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
// C, B could update twice in a single batch if C's render enqueues an update
// to B (since B would have already updated, we should skip it, and the only
// way we can know to do so is by checking the batch counter).
updateBatchNumber++;

for (var i = 0; i < len; i++) {
// If a component is unmounted before pending changes apply, it will still
// be here, but we assume that it has cleared its _pendingCallbacks and
// that performUpdateIfNecessary is a noop.
var component = dirtyComponents[i];

// If performUpdateIfNecessary happens to enqueue any new updates, we
// shouldn't execute the callbacks until the next render happens, so
// stash the callbacks first
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;

var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var namedComponent = component;
// Duck type TopLevelWrapper. This is probably always true.
if (component._currentElement.type.isReactTopLevelWrapper) {
namedComponent = component._renderedComponent;
}
markerName = 'React update: ' + namedComponent.getName();
console.time(markerName);
}

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

if (markerName) {
console.timeEnd(markerName);
}

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

这个函数里面主要就是个嵌套遍历过程,第一层先遍历dirtyComponents对每个component调用ReactReconciler.performUpdateIfNecessary,然后第二层对当前component前期没有更新的callbacks进行遍历执行。

ReactReconciler.performUpdateIfNecessary主要是判断当前component._updateBatchNumber和updateBatchNumber是否相等 不相等直接退出,否则调用当前component自身performUpdateIfNecessary进行更新。

这个不相等直接退出是什么意思呢?假设dirtyComponents中队列为[A, B], 但是它们结构如下:

1
2
3
4
5
6
7
     +------+
| A |
+---------+
| |
+-----+ +------+
| B | | C |
+-----+ +------+

此时B会触发2次更新,显然这是一个重复的操作,那么问题是两次更新,抛弃哪一个更新呢?

这里仔细想一想react更新流程,很容易明白,当dirtyComponents进行操作维护的时候,基本都是处于正在更新中否则会直接进行更新,直到更新结束时候才会进行批处理对dirtyComponents中component进行更新。

当我们dirtyComponents中队列为[A, B]时候,相关的callback|state都已经在这个时间段中收集完毕了,不会出现更新过程中突然新加入一些callback|state的情况。

React在进行dirtyComponents更新时候会采用队列中第一次更新,而抛弃队列中后续相同组件的更新。

enqueueUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enqueueUpdate(component) {
ensureInjected();

// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)

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

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

这个函数是一个入队操作。

如果当前没有处于更新过程中,那么直接进行更新(batchingStrategy.batchedUpdates(enqueueUpdate, component))不加入dirtyComponents

否则将需要更新的component压入到dirtyComponents,并设置component._updateBatchNumber