Understanding The React Source Code - UI Updating (Transactions) VII

Last time we discussed the core Transaction class and ReactDefaultBatchingStrategyTransaction. The instantiation is not the sole Transaction involved in UI updating but merely the one that leads.

In this post, we are going to examine the other Transactions which, together with ReactDefaultBatchingStrategyTransaction, outline the UI updating processing logic.

Files used in this article:

renderers/shared/stack/reconciler/ReactUpdates.js: defines flushBatchedUpdates() that is the entry point method of this post; it also defines ReactUpdatesFlushTransaction, one of the Transaction s we are going to discuss

shared/utils/PooledClass.js: defines PooledClass for enabling instance pooling

renderers/dom/client/ReactReconcileTransaction.js: defines ReactReconcileTransaction, another Transaction we are going to discuss

We start with the ReactUpdates.flushBatchedUpdates() {last post *8}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
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

As mentioned before, it processes all the dirtyComponents by initiating a ReactUpdatesFlushTransaction that will invoke runBatchedUpdates eventually.

PooledClass - maintain an instance pool

The two uncommon methods getPooled(), release() are inherited from PooledClass, which provides the instance pooling capacity:

a) if there is no allocated instance (in this case, ReactUpdatesFlushTransaction) in the pool, getPooled() creates a new one;

b) if instances exist in the pool, getPooled() simply returned the instance;

c) release() does not release the instance, instead, it simply put the instance back to the pool.

Like any other kinds of pools, the end purpose of this instance pooling is to reduce the overhead of superfluous resource (in this case, memory) allocation and destruction.

Back to our specific case above:

1
2
3
4
5
...
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
...

ReactUpdatesFlushTransaction instances are allocated only when the first time the while loop is executed. After that instances can be obtained through getPooled() from the pool directly.

This level of understanding of PooledClass is sufficient for the rest of this post. So feel free to fast travel to the the next section by ctrl-f “ReactUpdatesFlushTransaction”.

Now we look at its implementation:

1
2
3
4
5
6
7
8
9
var PooledClass = {
addPoolingTo: addPoolingTo, // scr: ------> 1)
oneArgumentPooler: (oneArgumentPooler: Pooler), // scr: ------> 2)
...// scr: not used
};

module.exports = PooledClass;

PooledClass@shared/utils/PooledClass.js

1) addPoolingTo() is a “public” method that adds the pooling functionality to a class;

2) oneArgumentPooler() is the getPooled() underlying implementation.

Next we look at addPoolingTo()‘s function body:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
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 is the pool;

2) attach DEFAULT_POOLER (a.k.a., oneArgumentPooler) to the getPooled();

3) set poolSize to 10;

4) attach standardReleaser() to the release().

And this is how getPooled() and release() are implemented:

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
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) corresponds to b), in which

1
Klass.call(instance, copyFieldsFrom);

invokes Klass constructor with the designated parameters (copyFieldsFrom) to initialize the pooling enabled class; and

2) corresponds to a); and

3) corresponds to c).

Last, we look at how addPoolingTo() is used from the outside (ReactUpdatesFlushTransaction):

1
2
3
4
5
...
PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
...

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

ReactUpdatesFlushTransaction

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
...
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) it is another instantiation of Transaction that overrides the perform() method;

2) instead of calling ReactUpdate.runBatchedUpdates (the callback) directly, the overridden ReactUpdatesFlushTransaction.perform() nest calls another Transaction (ReactReconcileTransaction)’s perform() method and pass method (i.e., ReactUpdate.runBatchedUpdates()) as its callback. Note that ReactReconcileTransaction is also pooling enabled.

3) TRANSACTION_WRAPPERS defines its pre and post-functions:

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
...
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

in which, NESTED_UPDATES‘s initialize() 1.5) stores the number of dirtyComponents; its close() 3) check if the number has changed. If they are different {a} the flushBatchedUpdates() is called to reiterate the process; or {b} it set dirtyComponents.length back to 0, and returns back to the upper level Transaction, ReactDefaultBatchingStrategyTransaction {last post}.

I will not examine the CallbackQueue related operations (in UPDATE_QUEUEING) and will leave them for later articles when discussing component’s life cycle. *9

To recap:

ReactReconcileTransaction

It is another Transaction and nothing is out of ordinary.

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
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) Its wrappers are defined in TRANSACTION_WRAPPERS;

2) as mentioned before, this class is pooling enabled.

Next we look at its three wrapper:

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
66
67
68
/**
* 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

The comment is quite clear here:

SELECTION_RESTORATION is for storing the focus state of text fields before the UI updating (initialize()), and it restores the state after (close());

EVENT_SUPPRESSION is for storing the toggle state for enabling event, and disable event temporarily before (initialize()), and it restores the state after UI updating (close()).

Again, I will not examine the CallbackQueue related operations (in ON_DOM_READY_QUEUEING) here and will leave them for later articles when discussing component’s life cycle. *10

It is important to note that ReactReconcileTransaction relies on the default Transaction.perform(). Its callback is the ReactUpdate.runBatchedUpdates which is passed all the way down to this level.

ctrl-f “runBatchedUpdates” to examine its route.

And this ReactUpdate.runBatchedUpdates will lead to the content of my next post.

to recap:

Conclusion

That's it. Did I make a serious mistake? or miss out on anything important? Or you simply like the read. Link me on -- I'd be chuffed to hear your feedback.