state/setStateThe source code parsing

The introduction

Front knowledge

  • ReactIn thejsxRendering principles.

  • ReactIn aboutstateExisting “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:

  1. parsingReactIn thesetStateThe parsing process of.
  2. implementationReactIn thesetStateTrigger page re-rendering.
  3. Compose events and batch asynchronystateThe 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!

setStateThe 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 timeUpdaterThis class accepts the component instance in its constructorthis.instance.
  • Defines apendingStateThat’s every time we’ve talked about itsetSetateWill be newstatePush into a queue. Yes, that’s it.
  • Defines athis.callbacks.setStateThe second argument to supports an optional callback function, which we use herecallbacksTo 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=stateTo modify the internal component instancestateBecome up to datestate.
  • Calls from within the componentforceUpdate()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.

setStateprocess

In fact, we can see that the process is pretty clear so far:

How does setState trigger page updates in React

ReactIn thesetStatePage 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:

  1. We need the old onesVdomObject.
  2. Through the oldVdomObject we get this one on the current pageVdomRendered truthDOMElement, and itsparentNode.
  3. Get the latestVdomObject, by recallingrenderMethod to obtain.
  4. forDom-diffWe’re going to skip this step.
  5. Prior to passingcreateDomMethod to generate a new realitydomElements.
  6. callparentNode.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, aclassComponent, of this componentvdomWill not actually mount indomNode, of his instancerenderThe 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

wang.haoyu

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

renderOldVDomGet 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:

  1. 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 ().

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

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

throughnewVdomGenerate a newdomElements & 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

  1. We knowreactInternally, there is a variable that controls whether the batch update is asynchronous or synchronous. We need a global variable to control the update logic.
  2. Batch updates based on event handlers, we need to hijack.reactThat is, to delegate all events todocumentGo up. throughdocumentUnified execution and partial pre/post logic processing.
  3. Enable batch update identifier bits in event handler preconditions (reactModify global variables internally) -> Execute event handlers (we define our own) -> postfix functions (reactCall, 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
  • isBatchUpdatingYes Controls whether to enable batch update.trueTo open it.
  • updatersIt’s the one we definedsetObject to cache batch updated instances.
  • batchUpdateThe 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.

updateQueueImplement 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