Preact is a lite version of the React, will read the React of source code, we have to React to the understanding of design principle and design idea. Preact is a good idea at this point. We can start with Preact as a simple framework.

Before introducing Preact’s main rendering process, let’s take a quick look at what JSX is.

JSX profile

In React, JSX is a tag syntax recommended for use with React. It usually takes the following forms:

const element = <h1>Hello, world! </h1>;Copy the code

On the page, the content is rendered as an H1 tag that says Hello, World! Like the picture below.

So JSX is really just a JS expression, and you can use all of the JS syntax rules. This includes for loops, if expressions, and so on. It is in the packaging tool that we insert it into the page as an HTML node.

When React uses JSX expressions, the HTML tag or React tag converts all JSX into a JS object using the React. CreateElement function. This object records the attr, Events, and other attributes of the current DOM node and renders them to the page.

Using JSX improves browser rendering efficiency by avoiding unnecessary manipulation when the actual DOM is rendered onto the page.

A more detailed introduction to JSX can be found below:

Zh-hans.reactjs.org/docs/jsx-in…

Start with the rendering of a component

React is a component-based framework. The most common feature we use is to declare a component and render it on a page. Something like this:

class Home extends Component { constructor(props) { super(props); this.state = { title: 'Hello' } } changeTitle() { this.setState({title: 'changed'}); } render() {return (<div> <button onClick={this.changetitle.bind (this)}> </button> <h1>{this.state.title}</h1> </div> ); }}Copy the code

In this code, we declare a component as a class. In this component, we declare a render method to render the DOM node of the current component, and we bind a click event to the button element. The effect of this event is to change the copy in the following H1 tag that uses a state declared in the component.

To render this component to the page, we also need to call the render function declared in PReact. In this function, we pass in three parameters: the component we want to render, the parent node of the component, and the replacement node. We usually call the render function in the following way.

render(<Home />, document.body);
Copy the code

As you can see, we pass in two parameters here, the React component Home as a child element and a native DOM node document.body. The main function we do in render is to render the Home component onto the parent node. Let’s take a closer look at how this process works.

Creating a virtual DOM

After entering the render function, the first task is to check the type of the parentNode passed in to mount the target, parentNode. The current parent element node can only be a native HTML DOM. Within this, the author identifies three levels of ELEMENT_NODE DOCUMENT_FRAGMENT_NODE and DOCUMENT_NODE. These three types represent the element node, Fragment node, and document node in HTML respectively. If the passed parent does not meet the requirements, an error is thrown. The parent we pass in here is body, and in this case the type is a code Fragment, so its type is Fragment.

After the React component is checked, we’ll turn the React component into a virtual DOM and place it on the _children property of the current parent node. At that point, The Home component written in JSX syntax has been converted into a JS object containing its component name, component type, child elements, and other properties. The next step is to convert the current object into a virtual DOM.

Generating the virtual DOM

To convert an object of type Home into a virtual DOM, we need to look at createElement. In this function, createVNode is the focus. This is a function declared in PReact. Used specifically to convert a React component object into a virtual DOM node. This function takes five arguments, which are:

/**
 * Create a VNode (used internally by Preact)
 * @param {import('./internal').VNode["type"]} type The node name or Component
 * Constructor for this virtual node
 * @param {object | string | number | null} props The properties of this virtual node.
 * If this virtual node represents a text node, this is the text of the node (string or number).
 * @param {string | number | null} key The key for this virtual node, used when
 * diffing it against its children
 * @param {import('./internal').VNode["ref"]} ref The ref property that will
 * receive a reference to its created child
 * @returns {import('./internal').VNode}
 */
Copy the code

After passing in the basic component information, a basic vNode object is initialized. The values of the object represent the following:

Const vnode = {type, // The type of the current component, usually the constructor or name of the component to be generated, // the props property of the component, passed key, // The key property of the component to identify the consistency of the component, // children: null; // parent: null; // depth: 0, // The current node's HTML level depth _dom: null, // the current node's outermost HTML DOM _nextDom: undefined, // the current node's next HTML DOM _component: _hydrating: null, constructor: undefined, _original: original == null? ++vnodeId: original // Unique id of the current vNode};Copy the code

Add initial properties

Once the object is initialized, it is time to add various VNode-related properties to the object. The next step is to add the Proto property, or implicit stereotype, to the current virtual object. The added content is as follows:

The vNode’s type property is then checked. If it is not a string, then it is not a native HTML DOM, but a React node. Set vNode’s $$Typeof property to REACT_ELEMENT_TYPE. At this point, a vNode is generated, and the current vNode is a Fragment element with the actual element to render on its props. Children.

After generating the structure of the current DOM tree, we finally need to use the diff algorithm to compare the newly generated node with the original node, and render the result of the final comparison. But this is actually the first rendering of our component, so there are a lot of differences between the diff process and the updated diff algorithm.

The Diff algorithm

The Diff algorithm is a core algorithm in React. Its purpose is to avoid unnecessary DOM tree changes and rendering. Through the way of combining the diff algorithm and vNode, we can know in advance what DOM elements are changed, so as to update only these elements have changed, without the need for a global refresh, below, we will through the way of flow chart, to do a more detailed interpretation of this step, there is a flow diagram, And you can look at that.

Diff function (blue line)

First of all, we’re going to go into a function called diff, and the first thing we do in this function, again, is check. The first thing to do in this check logic is to extract the vNode’s type and parent, and the nearest parent to the current vNode. After getting the parent node, it will check whether the current parent node type is illegal, mainly checking whether it is the node related to table, so as to judge whether the parent node is compliant. It will also take the corresponding logical processing according to the different types of the parent element node.

Initialize the comp

After this series of checks and actions, we start checking the type of the new node that is passed in. If it is function, it will be treated as a React component. The id of the original vNode context is searched in the globalContext, and if no corresponding context is found in the globalContext, the globalContext is assigned to the vNode context.

To initialize a new component, first pass the props and context of the current vNode to the Componnet constructor, and then initialize an object called comp. Treat the current vNode’s Type property as comp’s constructor and initialize comp’s render function.

The first call to the lifecycle

Initialize comp with the state property as an empty object, and use _dirty to indicate that comp has not been updated and is a new component. Initialize _nextState to the value of state. Next comes the call to the life cycle before comP renders. The first thing to check is componentWillMount, which is called if the lifecycle function is defined on comP. The next step is to check componentDidMount for the life cycle, and if the life cycle is defined, the function will be placed in _renderCallbacks and called after render.

After that, set comp’s _dirty to false and _parentDom to the parent element passed in.

Gets the child elements of the component

Next comes the doRender operation, the main purpose of which is to get the child elements of the COMP. Comp’s render function returns the current props. Children. Since our comp is a Fragment component, this step returns the Home component.

The React node is then checked to see if the React node is a child node, and diff the child node.

The current stack frame is as follows:

Diff Children (marked green)

Compare newChildren and oldChildren

After we diff the first layer of nodes, we diff the child nodes. The first step of the diff child node is to fetch the oldChildren from the original oldNode and compare them with the children of the current component collected above to update them. However, we are currently rendering the child node for the first time, which means there is no oldNode, so there are fewer steps than the actual update.

The Fragment node’s child is a Home element, so the newOldChildren list contains only the current Home element.

So we first iterate through the data of the new child, and we get a child, newOldChildren, which is the Home element. Assign the parent value of the current child node to the current parent node, and assign the node depth of the current child node + 1 to indicate which layer the current node is at.

Then we get the old child node oldChildren with the same subscript. If oldChildren and newChildren have exactly the same key and type, then we directly use the new node. Set the value of this object to undefined and the current stack frame is as follows:

Then the diff function will be entered again, and the current oldChildren and newChildren will repeat the above diff function logic again. Calling the current render function of newChildren is equivalent to calling the render function of our Home component, which returns a JSX.

Render () {return (<div> <button onClick={this.changetitle.bind (this)}> </button> <h1>{this.state.title}</h1> </div> ); }Copy the code

According to this JSX, we use the createElement function to create the vNode of the div tag, which takes the child element of Home and again enters diffChildren.

After performing the same operation in diffChildren and entering the diff function, the diff function detects that the current vNode is already a native HTML tag instead of a React element. It then goes inside diffElementNodes.

Current stack frame

DiffElementNodes (orange lines)

The diffElementNodes function is primarily used to diff native HTML nodes. In this function, you need to pass in the real element node of oldNode, which is the HTML node of oldNode.

Create div node

First check the type of the current tag node. If the node is not SVG and is not empty, a corresponding node will be created using document.createElement based on the current element type. Here we create the parent div node of the first level.

Next we check to see if the current node has child elements and dangerouslySetInnerHTML. DangerouslySetInnerHTML is a property that inserts HTML fragments directly into the DOM. If we have this property, then we assign a _children property to it.

Check whether it is the bottom node

If not, we need to determine if the current node is already a “bottom” node. Because it depends on what we do next. If the page is already “bottom”, that means it doesn’t have any DOM children, but if it’s a DOM node or a React node, Diff the props on the old and new nodes and update the props that are not children and key. Then continue with diffChildren and repeat the above procedure.

In the current div element, there is also a button element and an H1 element, so again we enter diffChildren for comparison. After this step is complete, we finally return the new node div we created in this function to the diff function.

After returning to the diff function, check again whether the currently generated new DOM conforms to the specification and return to diffChildren. Once we get the new DOM element in diffChildren, we need to find a suitable place to put the current DOM element. In the case of the div element, we insert it into the parent node, or Home, And this is where we’ll find the next node to render.

Next, we set each reference in the list of oldChildren to undefined and call the unmount life cycle.

After unloading the previous sibling nodes, we can go back to the diff function at the first level and set the _base property of the current div-based component to the current DOM node. And finally take out the function to execute after rendering in the commitQueue.

The final stack frame and execution process are shown below:

Finally, our diff process is complete!

What happens after setState

Now let’s compare that to changing state, in setState.

First we pass an update parameter in setState, which is generally an object containing the key: value value to be changed.

this.setState({title: 'changed'});
Copy the code

First, a deep copy of nextState will be made in setState. Update state by traversing the original state and assigning the new state directly to the original state, so be careful if state is a reference. In this step, if no update to be changed is passed in, it is returned without re-rendering.

export function assign(obj, props) {
    for (let i in props) obj[i] = props[i];
    return obj;
}
Copy the code

Sort by depth

The next step is to put the task on the render queue, where the code will perform the render task based on the current task queue. The render task is not executed in the order in which it was placed in the render queue. Instead, the render queue is sorted in ascending order by the current node depth level (the _depth attribute), then the render queue is emptied and a copy of the previous queue is traversed, rendering the outer layer and then the inner nodes. If the _dirty value of traversing the current component is true, the rendering of the current component will be performed.

For the diff

Once again, the first step in component rendering is to diff the old and new components. Before we do diff here, we first need to do a series of preparatory work. In preparation, we take the _vnode property of the component whose state has changed as the new node. Then add one to the _original property of the new node as if it were the old node. And save the outermost DOM node on the old node as oldDom.

The same operation will be performed after entering the diff function, but the comP generation process is different from the above. When first rendered, comp is an instance generated using the Component constructor, but in the case of oldNode, comp is currently oldNode, and the updated state is assigned to _nextState. And save the props and state of the old component.

Execution life cycle

Next to comp defined on the life cycle for inspection, if you have any update new props and define componentWillReceiveProps, is called the current function. And then it checks if the current life cycle is defined, where componentWillUpdate is called at this point. ComponentDidUpdate is placed in the rendered callback function, commitQueue.

After calling componentWillUpdate, the PROPS of the COMp will be assigned to the value of the new component, and state will be assigned to _nextState on the new component. With the above preparations in place, we can start rendering the outermost components.

Since our current component updates rather than initializes the mount component, we don’t need to set a Fregment element on the outermost layer. Instead, we update the target component directly, so we can call the render function of the component directly:

Render () {return (<div> <button onClick={this.changetitle.bind (this)}> </button> <h1>{this.state.title}</h1> </div> ); }Copy the code

When executing this rendering function, the two functions of diffChildren and diffElementNodes are still used to recursively traverse from the uppermost DIV node to the H1 node, and update the props on each node when it is convenient. After traversing the H1 node, we will find that its child node is of type text. If we find that its props are different from the old node, we will directly assign the new props to the data attribute of the new node. That is, the diffElementNodes node diff all the way to the bottom text node, and then updates the text node.

The final render

In h1, we will find that the new copy is inconsistent with the old copy, so we will update the new copy to the node to trigger the page re-rendering of this node, and finally return to the diff function to complete the execution of all functions in the commitQueue. The current setState task is complete.

The above article gives us an understanding of the core logic and code details of React through PReact. Of course, the React code details are much more complicated now, especially with Fiber. In the next installment of this series, I’ll dive deeper into the React source code and show you more details.