state
/setState
The source code parsing
The introduction
Front knowledge
-
React
In thejsx
Rendering principles. -
React
In aboutstate
Existing “problems”.
The knowledge involved in this article is gradually explained and developed. Of course, if you are not interested in the content (already understood), you can also directly enter the content of this article, because each chapter will not have a strong coupling with the previous one.
For the code addresses involved in the article, go here to 👇.
The content of the article will be divided into two steps:
- parsing
React
In thesetState
The parsing process of. - implementation
React
In thesetState
Trigger page re-rendering. - Compose events and batch asynchrony
state
The update.
React doesn’t recommend class Component anymore, but it’s clear that it won’t remove class. The React component has a lot of react ideas, so it’s much easier to understand FC by starting with the Class component gradually, so we don’t have to use it now, but never underestimate the Class component!
setState
The principle of the process
The introduction
In the previous article we implemented JSX elements and components translated from Babel rendered from zero.
In react.js we implement code like this:
import { transformVNode } from './utils';
import { Component } from './component';
const React = {
// Focus on Component
Component,
// This logic is not relevant to this chapter and can be used directly
createElement: function (type, config, children) {
constprops = { ... config, };if (arguments.length > 3) {
props.children = Array.prototype.slice
.call(arguments.2)
.map(transformVNode);
} else {
props.children = transformVNode(children);
}
return{ type, props, }; }};export default React;
Copy the code
We all know that in React all class components require extends React.ponent.
First, realize that the Component is implementing its parent Component.
Component
When we call setState in class Component, there is no setState method in our custom component. In other words, the first thing we need to implement is this setState method definition.
At this point we’ll create a component.js in the react.js directory of the same level and import it.
component.js
:
class Component {
constructor(props) {
this.props = props;
/ / the default state
this.state = {};
}
// We said that the second argument callback is executed after the page is updated
setState(partialState, callback) {
//}}// Implement class component-specific properties
Component.prototype.isReactComponent = {};
export { Component };
Copy the code
The code above, Component. The prototype. IsReactComponent before we said in the react source class CMP and function CMP processed their vdom the type attribute is a function of type on the object (a pointer to a function itself, A class that points to itself.
The two components are distinguished by the presence or absence of the isReactComponent attribute on the prototype.
Moving on to the rest of the code, the Props in the constructor of the Component class that accepts passes from child components (when inherited from super) are not the point right now.
We then define the setState method and the state default empty object on the parent class.
Of course, we all know that setState can take two arguments, so here we accept two arguments as inputs.
Updater
.// We said that the second argument callback is executed after the page is updated
setState(partialState, callback) {
this.updater.addState(partialState, callback);
}
Copy the code
What is this.updater?
First, there is no state scheduling in react.component. in other words, it doesn’t control whether you update synchronously or asynchronously, it only manages component rendering based on component state.
So inside each component there is an instance object of updater that acts as the control state updater.
Each call to setState(partialState,callback?) Each is essentially passed to its own this.updater.addState(partialState,callback).
Let’s improve this code:
// Update class
class Updater {
constructor(componentInstance) {
this.instance = componentInstance;
this.pendingState = []; // Queue for asynchronous setState storage
this.callbacks = []; // The cache executes the callback function in batches}}class Component {
constructor(props) {
this.props = props;
/ / the default state
this.state = {};
// Each instance of a class component has its own Updater
this.updater = new Updater(this);
}
// We said that the second argument callback is executed after the page is updated
setState(partialState, callback) {
this.updater.addState(partialState, callback); }}Copy the code
We can see that in the Component constructor, we create an updater instance object and pass in the Component instance as an argument.
- At the same time
Updater
This class accepts the component instance in its constructorthis.instance
. - Defines a
pendingState
That’s every time we’ve talked about itsetSetate
Will be newstate
Push into a queue. Yes, that’s it. - Defines a
this.callbacks
.setState
The second argument to supports an optional callback function, which we use herecallbacks
To cache.
Updater
Next we implement the addState method, where arguments to each call to setState are forwarded to the addState method of the updater instance inside the Component.
The first thing we can think of is that all the addState method must do is push the latest setState change onto the stack and push the callback (if any).
class Updater {...// Update the status
addState(partialState, callback) {
this.pendingState.push(partialState);
if (isPlainFunction(callback)) {
this.callbacks.push(callback);
}
// When we are done pushing into the queue
// The emitUpdate() function fires
this.emitUpdate(); }}Copy the code
When using it, we know that every setSetate may cause page update, so we need to call trigger update after the execution of setState every time.
You can see that after the setState is added to the addState of the updater, the emitUpdate() method is called internally to trigger the update.
// props/state changes trigger the update
emitUpdate() {
// Add a batch update to determine if it is a batch update
this.updateComponent();
}
Copy the code
There are two issues here that need to be explained.
-
The updateComponent() method actually calls component updates, because we’re only dealing with state here, and we know that props changes also cause the page to render. Multilayer forwarding here is just for the convenience of expansion later.
-
We will then determine whether to batch asynchronously or synchronously in the updateComponent() function, and we will deal with synchronization first. That is, every time setState() triggers a page rerender.
Let’s implement the update logic:
// updateComponent
class Updater {...// Update the component
updateComponent() {
const { classInstance, pendingState } = this;
// There exists to be updated
if (pendingState.length > 0) {
// Let the component update
shouldUpdate(classInstance, this.getState()); }}}Copy the code
You can see that when pendingState is present, we call the shouldUpdate method to update the component instance.
Two arguments are passed, respectively
-
This.classinstance, which is the class component instance.
-
The second argument is the merged state, which is obtained by merging the instance’s this.getState() method.
Let’s drop the shouldUpdate() method and look at this.getState() first.
This method is actually quite simple now, we need to merge each setState in penddingState with the (old)state inside the component.
Note that the first argument to setState may be a callback.
So let’s implement it:
// utils
const toString = (value) = > Object.prototype.toString.call(value);
const isPlainObject = (value) = > toString(value) === '[object Object]';
const isPlainString = (value) = > toString(value) === '[object String]';
const isPlainNumber = (value) = > toString(value) === '[object Number]';
export const isPlainFunction = (value) = >
toString(value) === '[object Function]';
// Updater
class Updater {
// Get the current state
getState() {
let { state } = this.classInstance; // old State
const { pendingState } = this; // new State
// reduce accumulates state changes
pendingState.reduce((preState, newState) = > {
// if it is a function
if(isPlainFunction(newState)) { state = { ... preState, ... newState(preState) }; }else{ state = { ... preState, ... newState }; } }, state);// This is where the page should be rendered before the callbacks are called
this.callbacks.forEach((cb) = > {
cb();
});
/ / to empty
pendingState.length = 0;
this.callbacks.length = 0;
returnstate; }}Copy the code
Our this.getState() method clears the current pendingState queue and returns the latest state value.
Let’s implement shouldUpdate:
// Take two arguments, one component instance and the latest state
function shouldUpdate(instance, state) {
instance.state = state; // Change the state of the instance internally
// Call the instance's methods to re-render
instance.forceUpdate();
}
Copy the code
ShouldUpdate is actually doing something very simple inside,
instance.state=state
To modify the internal component instancestate
Become up to datestate
.- Calls from within the component
forceUpdate()
Method to update the component to re-render.
This. State = {… } will not trigger page rerendering, because there is no updater method and no component update method will be called if state is changed directly.
setState
process
In fact, we can see that the process is pretty clear so far:
How does setState trigger page updates in React
React
In thesetState
Page rerendering
Next we’ll focus on implementing the forceUpdate() instance method.
Let’s start with the idea of what this method needs to do, there is not much code, mainly is the update of the idea process:
State changes the rerender process
When we need to call the forceUpdate() method, the main thing is to call render() again with the state change to generate a new vDom, and then compare the old vDom object with the dom-diff to update the actual DOM element of the page. The main idea is as follows:
- We need the old ones
Vdom
Object. - Through the old
Vdom
Object we get this one on the current pageVdom
Rendered truthDOM
Element, and itsparentNode
. - Get the latest
Vdom
Object, by recallingrender
Method to obtain. forWe’re going to skip this step.Dom-diff
- Prior to passing
createDom
Method to generate a new realitydom
Elements. - call
parentNode.reaplce(newDom,oldDom)
Complete the component replacement and re-render the page.
Looking at the process in general, it’s essentially finding/generating the real DOM via vDOM and then finding the parent node and replacing it with the parent node.
renderVdom
& Vdom
RenderVdom and Vdom are two concepts that need to be understood before we start implementing them.
You can think of Vdom as a large set, and renderVdom is a subset of it. They are essentially Vdom objects, but each of them represents a different meaning.
Take a look at this code:
class MyComponent extends React.Component {
render() {
return <div>wang.haoyu</div>}}const element = <MyComponent />
ReactDOM.render(element)
Copy the code
In this code,
is a Vdom object. So what is renderVdom?
For example, aclass
Component, of this componentvdom
Will not actually mount indom
Node, of his instancerender
The element returned by the method is calledrenderVdom
.
MyComponent is a class component that returns a vDOM of type itself after the Babel swivel.
The render method of the MyComponent instance returns a
JSX that is also processed by Babel into a vDOM. However, the div node is converted into a real DOM and mounted on the page. We’ll just call it the renderVdom object.
implementation
To obtainoldRenderVDom
To get updates to a component, we first need to find the Vdom object for the corresponding component.
forceUpdate() {
// get the oldRenderVDom object from oldRenderVDom on the instance
const oldRenderVDom = this.oldRenderVDom
}
Copy the code
You might be wondering where this.oldRenderVDom came from.
In the previous JSX principles section, we implemented the method reactdom.render (vDOM), which recursively generates real DOM nodes from the VDOM.
When a class component is encountered, the mountclassComponent method is executed.
/ / mount ClassComponent
function mountClassComponent(vDom) {
const { type, props } = vDom;
const instance = new type(props);
const renderVDom = instance.render();
RenderVDom. OldRenderVDom = renderVDom
instance.oldRenderVDom = vDom.oldRenderVDom = renderVDom; // Mount the current RenderVDom on the class instance object
return createDom(renderVDom);
}
Copy the code
In the mountClassComponent method, new [Class](props) creates an instance object of the component and obtains the renderVdom object returned by the component via instance.render().
OldRenderVDom = renderVDom; renderVDom = renderVDom; renderVDom = renderVDom; We can of course get the renderVDom object in the parent class by calling this.oldRenderVDom.
If you haven’t read the previous article, just remember that there is an oldRenderVDom property on the classComponent instance that points to the renderVDom object returned by this.Render ().
renderOldVDom
Get realDom
Next we need to use this old renderOldVDom object to get the actual DOM element on the corresponding page. If you are careful, you may have noticed that in createDom we attach a Dom attribute to each vDom object, which points to the corresponding generated Dom.
With this DOM attribute, it’s easy to use the VDOM to get the actual DOM node on the corresponding page.
OldRenderVDom = this.oldrendervdom // oldRenderVDom = this.oldrendervdom // Const parentDom = findDom (oldRenderVDom).parentNode; }Copy the code
Let’s implement the findDom method and place it in the previous reactdom.js file:
RenderVDom is not a real render node if it is a class or Function. Continue to recursively find renderVDom nodes if it is a normal Dom node Returns the mounted DOM property * directly@param {*} vdom* /
export function findDOM(vDom) {
const { type } = vDom;
if (typeof type === 'function') {
// Non-ordinary DOM nodes are class components or functionComponent
return findDOM(vDom.oldRenderVDom);
} else {
returnvDom.dom; }}Copy the code
In fact, its implementation is very simple, essentially through the DOM properties of the VDOM to find the real DOM. Need extra attention is that we need to find the renderVDom if inside still have funcitonComponent/classComponent we always go to recursion, to know its real rendering on the page to find the corresponding vdom elements.
As you can see in the following code, when type is a function (meaning it is an FC /class component) it returns directly and does not mount the DOM attribute.
Take a look at these two methods mountFunction/mountClassComponent:
When calling a function component, class component:
-
In the case of a class component, we attach oldRenderVDom attributes to its instance object and to the class itself, pointing to the renderVDom object returned by its instance Render ().
-
In the case of FC, we call FC to mount the renderVdom it returns on the function itself.
Eventually, if we run into an FC or classComponent, we just need to recurse its oldRenderVDom property to find vDOM objects that can actually be rendered to the page.
When we get the renderVDom object, we can use renderVdom.dom to get the actual DOM element that it renders on the page. Then get his parent container via parentNode.
Get the latestVdom
object
This step is actually quite simple, we have updated the latest state on this in the forceUpdate() method. We simply call the render() method of the instance again to return the latest component instance.
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
// Actually mount the DOM node
const parentDom = findDOM(oldRenderVDom).parentNode;
// Call the render() method again and get the latest Vdom object
const newRenderVDom = this.render();
}
Copy the code
forDom-diff
This step is omitted here and will be supplemented later.
Advanced simple and crude component overall replacement, the process through.
Let’s first define the render and diff method, which is in react-dom.js:
/** */ export function compareToVDom(parentDom, oldVDom, newVDom) { // ... }Copy the code
// Component.js
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
const newRenderVDom = this.render();
// Actually mount the DOM node
const parentDom = findDOM(oldRenderVDom).parentNode;
// The diff algorithm compares the differences and updates them to the real DOM
compareToVDom(parentDom, oldRenderVDom, newRenderVDom);
}
Copy the code
throughnewVdom
Generate a newdom
Elements & Render pages
The core rendering is done in the compareToVDom method, which is the core method for performing dom-diff and replacing page DOM elements.
Leaving dom-Diff behind, let’s start implementing updated page elements. (Here is a crude direct substitution — component state changes — re-rendering the component — replacing the dom corresponding to the component on the page with the DOM object generated by the new VDOM).
export function compareToVDom(parentDom, oldVDom, newVDom) {
// Find the actual DOM node on the corresponding oldVDom page by calling findDOM(oldVDom)
const oldDom = findDOM(oldVDom);
// Convert the new VDom object to the real DOM using the createDOM method
const newDom = createDom(newVDom);
// The parentNode.replace on the page replaces the old DOM element with a new DOM object to complete the page update
parentDom.replaceChild(newDom, oldDom);
}
Copy the code
Finally, don’t forget to update the renderVDom object on the instance to the updated VDOM:
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
const newRenderVDom = this.render();
// Actually mount the DOM node
const parentDom = findDOM(oldRenderVDom).parentNode;
// The diff algorithm compares the differences and updates them to the real DOM
compareToVDom(parentDom, oldRenderVDom, newRenderVDom);
// Update the vDom property on the instance to the latest note renderVDom
this.oldRenderVDom = newRenderVDom;
}
Copy the code
over
Here we can already complete – component call setState—-state change —- page corresponding to component update!
At present, we can run this process, and the Demo and the completion code are here.
But our current setState is only synchronous, and every call to setState is synchronous, that is, a call to setState will trigger a page rendering.
Next we implement composite events and asynchronous batch updates.
Compose events and asynchronystate
In the previous step we implemented the click-trigger event ->setState-> page refresh.
When we click on an element on the page to trigger the corresponding event function, the function internally modifies the value of state with setState and invokes the forceUpdate instance to refresh the page.
But the logic we are implementing now,setState only supports synchronous call refresh, does not support asynchronous batch update. That is, every call to setState updates setState in real time and is reflected on the page.
This is certainly not what we want. For details on when state is asynchronous and when it is synchronous in React, see [this article] (juejin.cn/post/700074…
Next, let’s first sort out the details to be implemented roughly from the idea:
The process to comb
- We know
react
Internally, there is a variable that controls whether the batch update is asynchronous or synchronous. We need a global variable to control the update logic. - Batch updates based on event handlers, we need to hijack.
react
That is, to delegate all events todocument
Go up. throughdocument
Unified execution and partial pre/post logic processing. - Enable batch update identifier bits in event handler preconditions (
react
Modify global variables internally) -> Execute event handlers (we define our own) -> postfix functions (react
Call, close the identity bit, perform a batch update of the cache). -> Refresh the page.
updateQueue
Let’s start by defining a global variable updateQueue in component.js to control whether batch updates are made:
// Component.js
// Control batch update
export const updateQueue = {
// Control whether to batch update
isBatchUpdating: false.// Cache batch update instances
updaters: new Set(),
// Batch update method
batchUpdate(){}};Copy the code
isBatchUpdating
Yes Controls whether to enable batch update.true
To open it.updaters
It’s the one we definedset
Object to cache batch updated instances.batchUpdate
The method is to perform a batch of previously cached emptyingstate
.
Updater
Next we will modify the Updater class to support batch update logic:
// Manage update scheduling logic
class Updater {
constructor(classInstance) {
this.classInstance = classInstance;
/ / state queues
this.pendingState = [];
/ / callback queues
this.callbacks = [];
}
// Update the status
addState(partialState, callback) {
this.pendingState.push(partialState);
if (isPlainFunction(callback)) {
this.callbacks.push(callback);
}
this.emitUpdate();
}
// props/state changes trigger the update
emitUpdate() {
if (updateQueue.isBatchUpdating) {
// Batch update
updateQueue.updaters.add(this);
} else {
// Non-batch update
this.updateComponent(); }}// Update the component
updateComponent() {
const { classInstance, pendingState } = this;
// There exists to be updated
if (pendingState.length > 0) {
// Let the component update
shouldUpdate(classInstance, this.getState()); }}// Get the current state
getState() {
let { state } = this.classInstance; // old State
const { pendingState } = this; // new State
pendingState.reduce((preState, newState) = > {
if(isPlainFunction(newState)) { state = { ... preState, ... newState(preState) }; }else{ state = { ... preState, ... newState }; } }, state);// This is where the page should be rendered before the callbacks are called
this.callbacks.forEach((cb) = > {
cb();
});
/ / to empty
pendingState.length = 0;
this.callbacks.length = 0;
returnstate; }}Copy the code
We can see that the main changes are to the emitUpdate method to support batch updates.
If updateQueue isBatchUpdating is true open batch update logo then will update current instance into updateQueue. Updaters.
ps: Each call to setState first calls addState(partialState,callback) of the updater instance of the current component, pushing the latest changes and callback into the pendingState and callback cache of the updater instance.
We modified the emitUpdate logic to cache instances in Updatequue. Updates if batch updates are enabled. If not, go to the previous direct update logic.
The event agent
We have implemented that every call to setState on the Updater instance is marked with isBatchUpdating to determine whether the batch update logic is entered.
So when do we start isBatchUpdating and when do we turn it off?
Let’s implement the event broker in React.
In React, all events are executed on document via event proxy. In this way React can hijack our events, adding some pre/post logic to the event execution functions.
Let’s start with the react-dom.js, which binds events directly to the corresponding element when handling events. This is clearly unreasonable
import { addEvents } from './events.js'
// react-dom.js
/ / update the props.function updateProps(dom, oldProps, newProps) {
Object.keys(newProps).forEach((key) = > {
if (key === 'children' || key === 'content') {
return;
}
// Events are processed incrementally using composite events and event delegates
if (key === 'style') {
addStyleToElement(dom, newProps[key]);
} else if (key.startsWith('on')) {
// Redefine an addEvents method to use to broker events
addEvents(dom, key.toLocaleLowerCase(), newProps[key]);
// addEventToElement(dom, key.toLocaleLowerCase(), newProps[key]);
} else{ dom[key] = newProps[key]; }}); }...Copy the code
Let’s implement the addEvents method:
Create a new event.js file
import { updateQueue } from './component';
/** * implements event delegation, binding all event hijacking to the root element *@export
* @param {*} Dom event element *@param {*} EventName eventName *@param {*} EventHandler event function */
export function addEvents(dom, eventName, eventHandler) {
// Mount the corresponding event attribute for the element
let store = dom.store ? dom.store : (dom.store = {});
store[eventName] = eventHandler;
if (!document[eventName]) {
// If there are many identical events such as click
// Then the event delegate will only delegate once
document[eventName] = dispatchEvent;
}
function dispatchEvent(event) {
let { target, type } = event;
const eventType = `on${type}`;
// Start time tart
const syntheticEvent = createSynthetic(target);
// Enable batch update
updateQueue.isBatchUpdating = true;
// Implement the event handler call notice the event bubble implementation
while (target) {
const { store } = target;
const eventHandler = store && store[eventType];
// Execute the event handler
eventHandler && eventHandler.call(target, syntheticEvent);
// Recursive bubbling
target = target.parentNode;
}
// Turn off the batch update flag
updateQueue.isBatchUpdating = false;
// Perform batch updatesupdateQueue.batchUpdate(); }}// The synthetic event target source code for additional event compatibility processing
function createSynthetic(target) {
const result = {};
Object.keys(target).forEach((key) = > {
result[key] = target[key];
});
return result;
}
Copy the code
In fact, the implementation idea is very simple. First, add a store object to each DOM node, which will contain the event name as key and the corresponding event handler value bound to the current React node. Such as
store = {
onclick: function () {
// do something
},
ontouchmove: function () {
// do something}... }Copy the code
Proxies are then made by uniformly listening for events on docuemnt.
Execute the dispatchEvent method when an event is triggered on document, such as clicking on a page element and triggering document.onclick.
The dispatchEvent method gets the actual element clicked, event.target, then gets the current DOM element of event.target, and gets the event handler that should be fired via dom.store[eventType].
Of course we did isBatchUpdating before and after the event handler, so before the event handler -> start bulk updating, after the event handler -> close is false.
The extra note here is that when we fire the event. Target event, we also need to restore the recursive bubbling up to look up the corresponding parentNode to trigger the event bubbling, triggering the event of the parent element.
updateQueue
Implement batch updates
When we implement the asynchronous batchUpdate via the event broker method, we execute updatequeue.batchupdate () at the end of the event broker function to do the batchUpdate. Let’s go back to component.js to refine this function.
// Control batch update
export const updateQueue = {
isBatchUpdating: false.updaters: new Set(),
// Batch update method
batchUpdate() {
for (let updater of updateQueue.updaters) {
updater.updateComponent();
}
updateQueue.isBatchUpdating = false; updateQueue.updaters.clear(); }};Copy the code
BatchUpdate () is a very simple function. By iterating through the updatequue. Updaters set, And then just go ahead and update ecomponent using the penddingState method of each updater.
conclusion
In fact, react asynchronous event updates are generally relatively simple to implement.
First of all, asynchronous update is determined by the flag bit whether asynchrony is enabled. Second, when the event is triggered, each event execution is placed on the document handler function to execute by the event broker.
When the event handler on the Document executes, it first updates the flag bit to enable batch updating, and then finds the corresponding event function to execute through event.target.store[eventType].
At the same time, recursive upward lookup is carried out to achieve the bubbling execution of the event.
Finally, when all bubbling ends, the identification bit is closed for unified batch update.
Finally, let’s look at the Demo we implemented:
import React from './react/react';
import ReactDOM from './react/react-dom';
// Class Component address
class ClassComponent extends React.Component {
constructor() {
super(a);this.state = {
number: 0}; } handleClick =() = > {
this.setState({ number: this.state.number + 1 });
console.log(this.state.number);
this.setState({ number: this.state.number + 1 });
console.log(this.state.number);
setTimeout(() = > {
console.log('Start timer');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
});
};
handleClickParent = () = > {
console.log('the parent - the parent trigger');
};
handleParent = () = > {
console.log('the parent trigger'.this.state.number);
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'the state of the parent');
};
render() {
// console.log(this.state.number, 'render');
return (
<div onClick={this.handleClickParent}>
<div onClick={this.handleParent}>Father elements<div onClick={this.handleClick}>{this.state.number}</div>
</div>
</div>); }}const element = <ClassComponent></ClassComponent>;
ReactDOM.render(element, document.getElementById('root'));
Copy the code
The final result when we click 0 on the page:
blow