Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Understanding The React Source Code — UI Updating (Transaction) VI
Photo by Jacob Ufkes on Unsplash
To some extent, the sophisticated and efficient UI updating is what makes React React. Before we dive into the mechanisms that empower the UI updating such as virtual DOM and diffing algorithm, we need to understand Transaction that transfers the control from a setState() to all those mechanisms.
Files used in this article:
renderers/shared/utils/Transaction.js: defines the core Transaction class
renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js: defines ReactDefaultBatchingStrategyTransaction and its API wrapper ReactDefaultBatchingStrategy
renderers/shared/stack/reconciler/ReactUpdates.js: defines the enqueueUpdate() that uses ReactDefaultBatchingStrategy
Unlike the previous posts that start from everyday APIs and move down the call stack. This post will move from bottom up, which I believe is a better approach to understand what is, and how to use Transaction.
So firstly, we look at the
Transaction the core class
The only de facto “public” method of this class is perform that also offers the core functionality:
.../**... * * @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
Besides the invocation of the callback method passed to it as the first argument, perform simply 1) invokes initializeAll() before the callback and 2) closeAll() after.
Here the errorThrown is used to indicate an exception occurred within method.call(), in which case the logic jump directly to finally block before errorThrown is set to false.
Next we look at those two methods,
... 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
These two methods simply iterate this.transactionWrappers and call their initialize() and close() respectively.
The this.transactionWrappers is initialized in the de fecto constructor of Transaction :
... 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
with this.getTransactionWrappers(). We will see what exactly are those this.transactionWrappers very soon.
The exception handling detail here is a bit interesting. Take initializeAll() as an instance, in the case that an exception occurs within initialize(), there is no catch block to handle the exception. Instead, after a finally block processes the initialize() of the rest of (i.e., from i + 1 to transactionWrappers.length-1) this.transactionWrappers, the exception interrupts the for loop and the entire initializeAll() logic. Then the logic processes all the way to the finally block within initializeAll()’s caller, perform(), which invokes closeAll() as normal.
Now we know what is a Transaction in its essence, but what is it used for? In order to answer this question, we take a Transaction instantiation as an example that is the transactional entry point of UI updating.
`ReactDefaultBatchingStrategyTransaction`
Firstly ReactDefaultBatchingStrategyTransaction is a subclass of Transaction that implements getTransactionWrappers():
...Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, { getTransactionWrappers: function() { return TRANSACTION_WRAPPERS; },});...
ReactDefaultBatchingStrategyTransaction@renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js
Next, TRANSACTION_WRAPPERS are the source of this.transactionWrappers that offers the pre (initialize()) and post (close()) functions for perform() in the last section.
...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) in the constructor of ReactDefaultBatchingStrategyTransaction the super class Transaction’s constructor gets called, which initializes this.transactionWrappers with FLUSH_BATCHED_UPDATES defined in 2)
2) defines the two wrapper and their respective initialize() and close(), which is used in the Transaction.initializeAll() and Transaction.closeAll() in the loops iterating FLUSH_BATCHED_UPDATES
3) defines ReactDefaultBatchingStrategyTransaction as a singleton.
Last we look at the public API offered by ReactDefaultBatchingStrategy that can be called from the outside world
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
as well as its outside world caller, the underlying method of the UI updating entry point 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
Here is a similar recursion trick as we saw in last post.
1) When the method is entered the first time, batchingStrategy.isBatchingUpdates is false, which triggers {a} that calls batchingStrategy.batchedUpdates() ;
2) batchedUpdates() sets batchingStrategy.isBatchingUpdates to true, and initializes a transaction ;
3) the callback argument of batchedUpdates is enqueueUpdate() itself, so enqueueUpdate will be entered again with transaction.perform straight away. Note that the pre-methods (initialize()) of both wrappers are emptyFunction so nothing happens between the recursions of enqueueUpdate();
4) when enqueueUpdate() is entered the second time (within the context of the transaction just initialized), it simply execute branch {b};
...dirtyComponents.push(component);...
5) and post-method (close()) of FLUSH_BATCHED_UPDATES is called after enqueueUpdate() returns; This is the workhorse method that processes all the dirtyComponents marked in the previous step(s)
*8 we will come back to this FLUSH_BATCHED_UPDATES.close() and ReactUpdates.flushBatchedUpdates() in the next post
6) post-method (close()) of RESET_BATCHED_UPDATES is called, which sets batchingStrategy.isBatchingUpdates back to false and complete the circle.
It is important to note that any calls of enqueueUpdate() between 3) and 6) will be in the context of Transaction.perform(), meaning, branch {b} will be taken.
Wrap-up
I hope you like this read. If so, please clap for it or follow me on Medium. Thanks, and I hope to see you the next time.👋
Originally published at holmeshe.me.
Understanding The React Source Code VI was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.