After joining the new team, React Native was used for team projects. In addition to learning how to use React Native, you need to understand the framework of React. Js to better use React Native. However, in the React framework, the author felt that the setState method seemed to be synchronous but not necessarily synchronous at the very beginning. After reading some articles on the Internet, the conclusions seem to be drawn from several blogs, but I still feel that there are some things I don’t understand. Fortunately, the source code of React. Look down the source code, some perplexing problems finally some.
Here are some questions to consider before you begin:
- Is setState asynchronous or synchronous
- Why is the value of setState called in the setTimeout method updated immediately
- Why does a Function pass in setState immediately update the value
- Why is setState designed this way
- What are the best practices for setState
React 16.4.0 is the latest version of React 16.4.0. The class name and file structure of React 16.4.0 have been changed a lot, but the design concept is still similar.
The entry of the setState
The top layer of setState is naturally in the act baseclasses.
//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
// ...
// Call the internal updater
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code
And this updater, as we were told at initialization, is actually injected at use.
//ReactBaseClasses.js
function ReactComponent(props, context, updater) {
// ...
// Real updater is injected in renderer
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
Copy the code
Find where to inject. The goal is to find out what type of updater is.
//ReactCompositeComponet.js
mountComponent: function(transaction, hostParent, hostContainerInfo, context,) {
// ...
/ / by the Transaction, this is ReactReconcileTransaction
var updateQueue = transaction.getUpdateQueue();
// ...
inst.updater = updateQueue;
// ...
}
Copy the code
//ReactReconcileTransaction.js
getUpdateQueue: function() {
return ReactUpdateQueue;
},
Copy the code
Finally, you see the contents of the specific enqueSetState method.
//ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState, callback, callerName,) {
// ...
// External examples are converted to internal examples
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
// ...
// Put the state that needs to be updated into the wait queue
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
// ...
// Callback is also placed in the wait queue
if(callback ! = =null) {
// ...
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
}
enqueueUpdate(internalInstance);
},
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
Copy the code
The update operation is handled by the ReactUpdates class.
//ReactUpdates.js
function enqueueUpdate(component) {
/ /...
if(! batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component);return;
}
dirtyComponents.push(component);
/ /...
}
Copy the code
The isBatchingUpdates measure is whether or not a batch update is being made. If you are updating, the entire component is put into the dirtyComponents array, as discussed later. Here the batchingStrategy, is actually ReactDefaultBatchingStrategy (external injection).
//ReactDOMStackInjection.js
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
Copy the code
In this class, suspend updates the state and call Transaction’s Perform.
//ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false.batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// Normal cases do not go into if
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e); }}};Copy the code
Transaction
Here is a brief explanation of the concept of Transaction, starting with the source code for a Transaction diagram.
//Transaction.js
* <pre>* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + *</pre>
Copy the code
In simple terms, a transaction is a collection of hooks before and after anyMethod is executed.
You can easily do things before and after a method, and you can have wrapper definitions, one wrapper and one hook.
In particular, we can define initialize and close methods inside the Wrapper. Initialize is executed before anyMethod and close is executed after.
Update Transaction in the policy
Back to the batchedUpdates method, the transaction is empty until it executes, and the callback is the enqueueUpdate method itself. When executing, isBatchingUpdates will get stuck adding dirtyCompoments. The close method is then used to change isBatchingUpdates and to flushBatchedUpdates.
//ReactDefaultBatchingStrategy.js
// Update the wrapper for status isBatchingUpdates
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};// The wrapper that actually updates the status
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
/ / two wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
// Add wrappers for transaction
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
returnTRANSACTION_WRAPPERS; }});var transaction = new ReactDefaultBatchingStrategyTransaction();
Copy the code
The flushBatchedUpdates method executes one transaction at a time based on the number of dirtyComonents in the flushBatchedUpdates method.
//ReactUpdates.js
var flushBatchedUpdates = function() {
while (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); }};Copy the code
This transaction is executed with the following hooks before and after.
//ReactUpdtes.js
// Start by synchronizing the number of dirComponents and end by checking to see if there are any new components added when the intermediate runBatchedUpdates method is executed
var NESTED_UPDATES = {
initialize: function() {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function() {
if (this.dirtyComponentsLength ! == dirtyComponents.length) { dirtyComponents.splice(0.this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0; }}};var TRANSACTION_WRAPPERS = [NESTED_UPDATES];
/ / add the wrapper
Object.assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
returnTRANSACTION_WRAPPERS; }});Copy the code
So the actual update method should be in runBatchedUpdates.
//ReactUpdates.js
function runBatchedUpdates(transaction) {
// Sort to ensure that the parent component is updated before the child component
dirtyComponents.sort(mountOrderComparator);
// ...
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
// Here we go to the update component methodReactReconciler.performUpdateIfNecessary( component, transaction.reconcileTransaction, updateBatchNumber, ); }}Copy the code
The performUpateIfNecessary method in ReactReconciler is just a shell.
performUpdateIfNecessary: function(internalInstance, transaction, updateBatchNumber,) {
// ...
internalInstance.performUpdateIfNecessary(transaction);
// ...
},
Copy the code
The real method is in the ReactCompositeComponent, which calls updateComponent if there is a state for the update in the wait queue.
//ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {
if (this._pendingElement ! =null) {
// ...
} else if (this._pendingStateQueue ! = =null || this._pendingForceUpdate) {
this.updateComponent(
transaction,
this._currentElement,
this._currentElement,
this._context,
this._context,
);
} else {
// ...}},Copy the code
This method makes some judgments, and we see that the value of nextState is the last value to be updated to state.
//ReactCompositeComponent.js
updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext,) {
// This merges the state in the sort queue to nextState
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
if (shouldUpdate) {
/ /...
} else {
// State has just been updated
/ /...
inst.state = nextState;
/ /...}}Copy the code
This method also explains why the state of the passed function is updated.
_processPendingState: function(props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
// The update can be set to null
this._pendingStateQueue = null;
var nextState = replace ? queue[0] : inst.state;
var dontMutate = true;
for (var i = replace ? 1 : 0; i < queue.length; i++) {
// If setState is passed in as a function, the received state is the state updated in the previous round
var partial = queue[i];
let partialState = typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial;
if (partialState) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState); }}}return nextState;
},
Copy the code
Looks like setState is synchronized
And if you follow this process, setState should be synchronous, right? What went wrong?
Don’t worry, remember that Transaction in the update policy. The callback from the middle call was passed in from the outer layer, which means that there may be other calls to batchedUpdates. So, the callback in the middle, it’s not just setState that causes it. A search of the code revealed that there were indeed several other places where the batchedUpdates method was called.
Take these two methods from ReactMount
//ReactMount.js
_renderNewRootComponent: function(nextElement, container, shouldReuseMarkup, context, callback,) {
// ...
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
// according to the current batching strategy.
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context,
);
// ...
},
unmountComponentAtNode: function(container) {
// ...
ReactUpdates.batchedUpdates(
unmountComponentFromNode,
prevComponent,
container,
);
return true;
// ...
},
Copy the code
Such as ReactDOMEventListener
//ReactDOMEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {
// ...
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
// ...}},//ReactDOMStackInjection.js
ReactGenericBatching.injection.injectStackBatchedUpdates(
ReactUpdates.batchedUpdates,
);
Copy the code
So for example, when called directly in componentDidMount, the **_renderNewRootComponent** method in reactmount.js has already been called, that is, The React component is rendered into the DOM in one big Transaction, and the callback is not executed immediately, so the natural state is not updated immediately.
Why is setState designed this way
In React, state stands for the state of the UI, that is, UI=function (state). In my opinion, this reflects a responsive thinking. The difference between responsive and imperative is that imperative focuses on the process of how to order, while responsive focuses on the output of data changes. In React, efforts made to Rerender, optimization of rendering, and responsive setState design are also indispensable parts of the process.
The last
The first question, I believe that everyone has their own answer, HERE I give my own understanding.
Q: Is setState asynchronous or synchronous?
A: Synchronous, but sometimes asynchronous.
Q: Why is the value of setState called in the setTimeout method updated immediately?
A: Because it is synchronous itself, there is no other factor blocking.
Q: Why is the value of a Function passed in setState updated immediately?
A: The strategy in the source code.
Q: Why is setState designed this way?
A: To change the UI in A responsive way.
Q: What are the best practices for setState?
A: Use it in A responsive way.
Refer to the link
React source code analysis series – Decrypt setState
SetState: What about the API design
Why does setState not update component state synchronously