React parsing implementation

React parsing implementation

premise

Before React 17, virtual dom objects were generated using the createElement function

After the jsx syntax is parsed by Babel, a virtual dom object is obtained through createElement. For example

Babel parsing is parsed when webpack is packaged and compiled, and then the parsed code is generated when the browser executes the createElement function to generate a virtual dom object

<div className="test">zzzz<span>dddd</span></div> ---> Babel parsing-----------------> React.createElement("div", { className: "test" }, "zzzz",/*#__PURE__*/React.createElement("span", null, "dddd")); ----> React.createElement is converted to a virtual dom object const vdom = { props: { className:'test', children: "zzzzs", //child may be a string or an array //children: ["zzzzs",{ //type:'span', //props: { //Attributes //} //}] }, type:'div' } Copy code

React 17

Implement react.CreateElement

function createElement (type, config, children) { if(config) { delete config.__self delete config.__source } const props = {...config}; //When there are multiple child nodes if (arguments.length> 3) { //children is an array children = Array.prototype.slice.call(arguments, 2) } props.children = children; return { type, props, } } const React = { createElement, } export default React; Copy code

react

react-dom

There is a render method to convert virtual dom to real dom, and then mount it to the container

render method implementation

Create real dom based on virtual dom

Hang the attributes of the virtual dom to the real dom

Make the child nodes of the virtual dom also become the real dom and hang on your own dom

Mount dom on the container

//Pass in a virtual dom object. Render with a container function render(vdom, container) { //Convert virtual dom to real dom for rendering const dom = createDom(vdom) container.appendChild(dom) } //Convert virtual dom to real dom function createDom(vdom) { //If it is a number or a string, directly return a real text node if(typeof vdom ==='string' || typeof vdom ==='number') { return document.createTextNode(vdom) } const {type, props} = vdom; //Create real dom const dom = document.createElement(type); //Update the virtual dom attribute to the real dom, updateProps(dom, props) //Turn the son of the virtual dom into a real dom and hang it on your own dom. if (typeof props.children ==='string' || typeof props.children ==='number') { dom.textContent = props.children } else if (typeof props.children ==='object' && props.children.type) { //There is only one child element, and the son is a virtual dom, insert the son into the real dom and insert it into the dom render(props.children, dom) } else if (Array.isArray(props.children)) { //If children is an array updateArryChild(props.children, dom) } else { document.textContent = props.children? props.children.toString(): "" } //Put the real dom as a dom attribute on the virtual dom, ready for future updates //vdom.dom = dom; return dom; } //Hang the props attribute of the virtual dom on the real dom /** * * @param {*} dom real dom * @param {*} props virtual dom properties */ function updateProps(dom, props) { for (let key in props) { if (key ==='children') continue; if (key ==='style') { let styleObj = props.style; for (let attr in styleObj) { dom.style[attr] = styleObj[attr] } } else { dom[key] = props[key] } } } /** * * @param {*} When the child child of the childVdom virtual node is an array * @param {*} parantdom parent node */ function updateArryChild(childVdom, parantdom) { for (let i = 0; i <childVdom.length; i++) { let childVdoms = childVdom[i] //Hang the virtual node on the parent node render(childVdoms, parantdom) } } const ReactDOM = { render, } export default ReactDOM; Copy code

Implement Component

Child elements are components

So make a judgment when you get the real dom, and then use different logic to get the real dom

-//Create real dom -const dom = document.createElement(type); +//Create real dom + let dom; + if (typeof type ==='function') { + if (type.isReactComponent) { +.//If it is a class component + return createDomClassComponent(vdom) +} else { + return createDomFunctionComponent(vdom)//Function component +} +} else { + dom = document.createElement(type);//ordinary +} Copy code

Function component

Create real dom react-dom.js

/** * * @param {*} vdom converts the virtual dom of the function component to the real dom */ function createDomFunctionComponent(vdom) { const {type: functionType, props} = vdom; //The function component is essentially a function, the execution gets the virtual dom const renderDom = functionType(props) //convert it to real dom return createDom(renderDom) } Copy code

Class component

Class components inherit Component, create a new component.js file, or write to the react file

import ReactDOM from'./React-dom' class Component { static isReactComponent = true;//Used to determine whether it is a class component constructor(props) { this.props = props; this.state = {} } } export default Component Copy code

Create real dom react-dom.js

/** * * @param {*} vdom converts the virtual dom of the class component to the real dom */ function createDomClassComponent(vdom) { //Deconstruct the definition of the class and the attribute object of the class const {type, props} = vdom; //Create an instance of the class const classInstance = new type(props) //Call the render method of the instance to return the virtual dom to be rendered const renderVdom = classInstance.render() //Create a real dom object based on the virtual dom object const dom = createDom(renderVdom) //To update the class component, hang the real dom on the instance classInstance.dom = dom; return dom } Copy code

Update class components

Hook the event to the react-dom file on the component first

//Hang the props attribute of the virtual dom on the real dom /** * * @param {*} dom real dom * @param {*} props virtual dom properties */ function updateProps(dom, props) { for (let key in props) { if (key ==='children') continue; if (key ==='style') { let styleObj = props.style; for (let attr in styleObj) { dom.style[attr] = styleObj[attr] } +} else if (key.startsWith('on')) { +//handle the event + dom[key.toLocaleLowerCase()] = props[key]; } else { dom[key] = props[key] } } } Copy code

Added the setState method to the componetn.js file

import ReactDOM from'./React-dom' class Component { static isReactComponent = true; constructor(props) { this.props = props; this.state = {} } + setState(prevState) { + let state = this.state; + this.state = {...state, ...prevState} +//Regenerate virtual dom + const newVdom = this.render() +//update + updateClassComponent(this, newVdom); +} } /** * * @param {*} instance instance * @param {*} vdom virtual dom */ //Here is the direct rendering of the entire dom structure, no diff is used, we will compare later function updateClassComponent(instance, vdom) { //Take out the real dom of the class component last rendered const oldDom = instance.dom; const newDom = ReactDOM.createDom(vdom); //Use the new to point to the old oldDom.parentNode.replaceChild(newDom, oldDom); instance.dom = newDom; } export default Component Copy code

We write a class component

class ClassCounter extends Component { constructor(props) { super(props) this.state = {number: 0} } render() { const handleClick = () => { console.log('xxx') this.setState({number: this.state.number + 1}) } return ( <div onClick={handleClick}> <span>Class component</span> {this.state.number} </div> ) } } Copy code

Finally, it can be updated smoothly

Bulk update

In React, the update of events may be asynchronous, batch, not synchronous setState. For example, if the same setState is adjusted twice, it will only be executed once. After calling setstate, it will not update immediately, but will be cached and processed by the event function. After that, perform a batch update, update and re-render once, (so that there is no need to refresh the components frequently)

such as

const handleClick = () => { this.setState({number: this.state.number + 1}) console.log(this.state.number)//0 asynchronous update this.setState({number: this.state.number + 1}) console.log(this.state.number)//0 asynchronous update this.setState({number: this.state.number + 1}) console.log(this.state.number)//0 Asynchronous update, when the function is executed, the state becomes 1 //Call setState three times, in fact, there is no change, which is equivalent to calling only once setTimeout(() => { console.log(this.state.number,'---?')//1 this.setState({number: this.state.number + 1}) console.log(this.state.number)//2 Synchronous update this.setState({number: this.state.number + 1}) console.log(this.state.number)//3 synchronous update this.setState({number: this.state.number + 1}) console.log(this.state.number)//4 synchronous update } ,0) //So whether it is a macro task or a micro task is the same, queueMicrotask(() => { console.log(this.state.number,'---????')//1 this.setState({number: this.state.number + 1}) console.log(this.state.number)//2 this.setState({number: this.state.number + 1}) console.log(this.state.number)//3 this.setState({number: this.state.number + 1}) console.log(this.state.number)//4 }) } Copy code

SetState returns a function is the same

const handleClick = () => { this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//0 this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//0 this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//0 setTimeout(() => { console.log(this.state.number)//3 this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//4 this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//5 this.setState((lastState) => ({number: lastState.number + 1})) console.log(this.state.number)//6 },0) } Copy code

setState can also pass the second parameter as a callback function, this callback function will be executed when the function is executed

Conclusion: As long as the event processing function is managed by react, it is asynchronous. For example, in handlCkick, (event processing function, life cycle function), as long as it is not managed by react, it is synchronous

Set a batch update flag and store the updaters of each component. When the component is batch update, store the state, and when the flag becomes false, update the component directly

import ReactDOM from'./React-dom' export const updateQueue = { isBatchUpdate: false,//Whether it is currently in batch update mode, the default is false updaters: new Set() } class Updater { constructor(classInstance) { this.instance = classInstance;//instance of class component this.pendingState = [];//The state waiting to take effect, which may be an object or a function this.callBacks = []; } addState(prevState, callback) { //Awaiting update or effective status this.pendingState.push(prevState) //callback after status update if (typeof callback ==='function') { this.callBacks.push(callback) } if (updateQueue.isBatchUpdate) { //this is an instance of updater updateQueue.updaters.add(this)//If it is a batch update, cache the updater } else { //Otherwise, update the component directly this.updateComponent() } } //Update the component directly when the isBatchUpdate status is false updateComponent() { const {instance, pendingState, callBacks} = this; //If there is a state waiting to be updated, first calculate the new state if (pendingState.length> 0) { //Calculate the new state instance.state = this.computedState(); instance.updateStateComponet() callBacks.forEach(cb => cb()) } } //Calculate the new state computedState() { const {instance, pendingState, callBacks} = this; let {state} = instance; pendingState.forEach((nextState) => { //Determine whether the state waiting for update is an object or a function if (typeof nextState ==='function') { nextState = nextState.call(instance, state); } state = {...state, ...nextState} }) pendingState.length = 0; callBakcs.length = 0; return state; } } class Component { static isReactComponent = true; constructor(props) { this.props = props; this.state = {}; //Each component has one, and one component corresponds to an instance of updater this.updater = new Updater(this) } setState(prevState, callback) { //Save the state and callback of the state this.updater.addState(prevState, callback); } updateStateComponet() { //Regenerate virtual dom const newVdom = this.render(); //update updateClassComponent(this, newVdom); } } /** * * @param {*} instance instance * @param {*} vdom virtual dom */ function updateClassComponent(instance, vdom) { //Take out the real dom of the class component last rendered const oldDom = instance.dom; const newDom = ReactDOM.createDom(vdom); //Use the new to point to the old oldDom.parentNode.replaceChild(newDom, oldDom); instance.dom = newDom; } export default Component Copy code

Synthetic event

Use event proxy to package event events, which can be compatible with different browsers, and can also do some things before and after the event processing function, such as batch update of react events

When we mount the events on the virtual dom to the real dom, we perform event proxying and will process the events to be mounted

//React-dom.js import {CompositeEvent} from'./CompositeEvent' function updateProps(dom, props) { for (let key in props) { if (key ==='children') continue; if (key ==='style') { let styleObj = props.style; for (let attr in styleObj) { dom.style[attr] = styleObj[attr] } } else if (key.startsWith('on')) { -//handle events -//dom[key.toLocaleLowerCase()] = props[key]; +//Synthetic event + CompositeEvent(dom, key.toLocaleLowerCase(), props[key]) } else { dom[key] = props[key] } } } Copy code

Add a method to updateQueue to bind to the event

//component.js export const updateQueue = { isBatchUpdate: false,//Whether it is currently in batch update mode, the default is false updaters: new Set(), + batchUpdate() { +//Batch update, set isBatchUpate to true at the beginning of the react event, and set to false after execution + for (let updater of this.updaters) { + updater.updateComponent() +} + this.isBatchUpdate = false; +} } Copy code

Create a new CompositeEvent.js file to implement event proxy. No matter which dom element is bound to the event, it must be unified proxy to the document event and rewrite the event event.

import {updateQueue} from "./Component"; /** * * @param {*} dom real dom * @param {*} eventType event type * @param {*} handleEvent event processing function */ export function CompositeEvent(dom, eventType, handleEvent) { //Bind an onClick event callback function to dom, handleEvent is in handleclick //let store; //if(dom.store) { //store = dom.store //} else { //dom.store = {} //store = dom.store //} let store = dom.store || (dom.store = {}) store[eventType] = handleEvent;//store.onclick = handleClick if (!document[eventType]) {//No matter which dom element the event is delegated to, the event will be bound to the document event. document[eventType] = dispatchEvent;//document.onclick = dispatchEvent } } //Store the created synthetic event let eventStore = { bubbling: false, stopBubbling() { this.bubbling = true; console.log('Prevent bubbling') } } //event is a native event function dispatchEvent(event) { let {target, type} = event;//Which dom element of the event source button, type = click const eventType = `on${type}`; //In the React control event, batch update updateQueue.isBatchUpdate = true; //Create a synthetic event object based on the native event object eventStore = createCompositeEvent(event); //event bubbling while(target) { const {store} = target; const handleEvent = store && store[eventType]; //Execute event processing function handleEvent && handleEvent.call(target, eventStore); if (eventStore.bubbling) break; target = target.parentNode; } //Let the eventStore object be used every time to form a singleton mode for(let key in eventStore) { eventStore[key] = null; } updateQueue.batchUpdate() } function createCompositeEvent(event) { for (let key in event) { eventStore[key] = event[key] } return eventStore } Copy code

Lifecycle version 15

  1. Implement componentWillMount
  2. Implement componentDidMount (execute after the component is rendered on the page, so save it to the real dom first, and execute when the real dom is mounted)
//react-dom //Pass in a virtual dom object. Render with a container function render(vdom, container) { //Convert virtual dom to real dom for rendering const dom = createDom(vdom) container.appendChild(dom) +//Execute after component rendering + dom.componentDidMount&& dom.componentDidMount() } /** * * @param {*} vdom converts the virtual dom of the class component to real dom */ function createDomClassComponent(vdom) { //Deconstruct the definition of the class and the attribute object of the class const {type, props} = vdom; //Create an instance of the class const classInstance = new type(props) //Life cycle + if (classInstance.componentWillMount) { + classInstance.componentWillMount(); +} //Call the render method of the instance to return the virtual dom to be rendered const renderVdom = classInstance.render() //Create a real dom object based on the virtual dom object const dom = createDom(renderVdom) //To update the class component, hang the real dom on the instance + if (classInstance.componentDidMount) { +//The component is executed after rendering, so it is executed when the component is mounted on the root node + dom.componentDidMount = classInstance.componentDidMount.bind(classInstance) +} classInstance.dom = dom; return dom } Copy code

shouldComponentUpdate

//component.js addState(prevState, callback) { //Awaiting update or effective status this.pendingState.push(prevState) //callback after status update this.callBacks.push(callback) -//State changes, state changes -//if (updateQueue.isBatchUpdate) { -////this is an instance of updater -//updateQueue.updaters.add(this)//If it is a batch update, cache the updater -//} else { -////Otherwise, update the component directly -//this.updateComponent() -//} + this.emitUpdate() } //Regardless of whether the component properties change or the state changes, it will update and emit updates emitUpdate (newprops) {//Pass a parameter, the property changes, that is, the props change if (updateQueue.isBatchUpdate) { //this is an instance of Updater updateQueue.updaters.add(this)//If the current batch update mode, cache the updater first } else { this.updateComponent();//Update the component directly } } //Update the component directly when the isBatchUpdate status is false updateComponent() { const {instance, pendingState, callBacks} = this; //If there is a state waiting to be updated, first calculate the new state if (pendingState.length> 0) { //Calculate the new state -//instance.state = this.computedState(); -//instance.updateStateComponet() callBacks.forEach(cb => cb()) + shouldUpdate(instance, this.computedState()) } } function shouldUpdate(instance, nextState) { //Regardless of whether the component needs to be refreshed or not, the state property of the component will definitely change instance.state = nextState; //If there is this method, and the method returns false, you do not need to continue to update if (instance.shouldComponentUpdate&&!instance.shouldComponentUpdate(instance.props,instance.state)) { return; } instance.updateStateComponet(); } Copy code

componentWillUpdate

componentDidUpdate

updateStateComponet() { + if (this.componentWillUpdate) { + this.componentWillUpdate() +} //Regenerate virtual dom const newVdom = this.render(); //update updateClassComponent(this, newVdom); + if (this.componentDidUpdate) { + this.componentDidUpdate() +} } Copy code

Lifecycle version 16

diff

------------------------------------>