Take you to write a Mini version of React

Take you to write a Mini version of React

Remember the last time I gave you Amway

build ur own react
That article? In fact, some classmates told me that it was tens of thousands of characters and it was all in English, and some didn t quite understand it, so I...summed up a Chinese version (according to my understanding of react and the upper part Some of the better things mentioned in his article), I hope it can be helpful to everyone

By the way, I would like to read this article again. The needle is not pricked. The able students can read my Chinese document and then read this English document: pomb.us/build-your-

Then let's go


One of the core purposes of this article is to take everyone with a source code architecture based on react. Let's implement a mini version of react from 0. Why is mini, because we grasp the big and let go of the small. The core functions of the are all implemented again, and the small ones are that we ignore some of the performance optimizations made by react in the source code and some functions that we usually use seldom or even don t use.

The function to be implemented in this blog post is based on React16.8

A function we want to achieve is roughly as follows:

  • createElement function
  • render function
  • Concurrent Mode & Fiber
  • render phase and commit phase
  • Reconciliation (diff algorithm in react)

Basic review

This section is mainly to review with you a basic working mode of React, JSX, and Dom. If you feel that you are already familiar with this section, then you can skip this section and go directly to the next section.

Let's see an example

const element = ( < div title = "foo" > hello, div </div > ); const container = document .getElementById( "root" ); ReactDOM.render(element, container); Copy code

The above code is completely based on an implementation of the React architecture, and he implemented a

div
Element rendering
id
for
root
The function in the real dom container

We don t need react now, we use native JS to achieve this function, you can stop and think about how you will achieve it

//The nice way to compare is that we can directly use an object to describe a JSX expression //Of course it does not limit your imagination, as long as you have any way to describe the above JSX expression //It can be rendered eventually All are OK const element = { type : "div" , props : { title : "foo" }, children : [ "hello, div" ], //Why is this children an array, because you think, we can write span tags and a tags directly in the div, this time we must use array to describe Him } const container = document .getElementById( "root" ); //This native JS provides support, so we don t need to convert at all //The role of the render method is to render the element into the container. We will not implement the render method itself for the time being. //Let s go into details if we want to render the element into the container ourselves const divDom = document .createElement(element.type) ; //In this way, we create a div node divDom.setAttribute( "title" , "foo" ); // Mark the attribute, in fact, you can directly use the form of div["title"] = "foo" const textNode = document .createTextNode(); textNode.nodeValue = element.children[ 0 ]; divDom.appendChild(textNode); container.appendChild(divDom); Copy code

OK, at this point, the above code provides technical support for us to achieve the same functions as React without using React. This is also a simple basic review. Now we can officially enter the topic.

createElement function

write

createElement
Before the function, we should know one thing, that is, the JSX expressions we write will eventually be compiled by babel
createElement
form

//For example, I have a line of JSX expression as follows: const element = ( < div className = "wrapper" > hello, wrapper </div > ); //He will eventually be compiled by babel into the following form const element = React.createElement( "div" , { class : "wrapper" }, "hello wrapper" ); //As for how you want to compile it, how can it compile, string replacement, let s leave it alone for the time being to copy the code

Through the previous review of the cognitive code, we should also have a basic understanding. In fact, in essence, the final connection point between React and real dom is that we need to have a

type
And an object with more properties, and
createElement
Is to provide us with a
element
Describe the object

So how do we design this function? You can stop and think about your own thoughts

//Let's fix the parameters first. //type: To create an object as mentioned above, we need to know the current node type //props: what properties are on the current node, it is an object //children: you can see what I use With the collection operator, it means that I have to collect all the remaining parameters into the children as its elements, and in this way we also ensure that children will always be an array.//Of course, you can also force the user to give it manually You pass a child, then your createElement //will always have only three parameters, and different writing methods can be used function createElement ( type, props, ...children ) { //Then we return all the properties according to the parameters.// Here I put the children into the props object again. It doesn t matter where you want to put it. Just make sure that the returned object has children in it. Just ok pull return { type, props : { ...props, children } } } Copy code

So let s try to write a more complicated structure and see if there are any problems.

//We have a JSX expression as follows const element = ( < div className = "wrapper" > < span class = "title" > I am the title </span > < input placeholder = "Please enter text"/> </div > ); //According to the expected idea, the above code will be converted by babel into the following form createElement( "div" , { class : "wrapper" }, React.createElement( "span" , { class : "title" }, "I am the title" ), React.createElement( "input" , { placeholder : "Please enter text" })); Copy code

In fact, we can see a problem, then it is the child node collected by the children of our createElement function. It can be an object created by createElement or an original value (

Primitive Value
)such as
Number
or
String
, This will cause a small problem. When we traverse the value of children in the future, we cannot safely determine whether the child element of children is an object. We need to make a logical decision.
if (Object.getPrototypeOf child element) === Object.prototype
, This is very annoying, so we'd better be in
createElement
It s done for him in the function, let all the elements in children no matter what you give me, what I save is an object

//I created a new method of createTextNode, which specifically generates text nodes for us //We know that only text nodes can be the original value. Here you can think about it function createTextNode ( textValue ) { return { type : "text " , props : { nodeValue : textValue, children : [] } } } //Then we slightly modify our createElement method function createElement ( type, props, ...children ) { return { type, props : { ...props, children : children.map( child => Object .getPrototypeOf(child) === Object .prototype? child: createTextNode(child)) } } } Copy code

We build our own

myOwnReact
Folder, create a
createElement.js
,
render.js
with
index.js
Folder, add our code separately, and we will introduce our own in the follow-up
myOwnReact
, And because compiling JSX itself is something that Babel assists React to do, so we will not describe how babel compiles JSX here (in fact, we will not use JSX at all, we will assume that Babel has been passed. The compilation becomes
createElement
Function).

//myOwnReact/createElement export function createTextNode ( textValue ) { return { type : "text" , props : { nodeValue : textValue, children : [] } } } export function createElement ( type, props, ...children ) { return { type, props : { ...props, children : children.map( child => Object .getPrototypeOf(child) === Object .prototype? child: createTextNode(child)) } } } Copy code
//myOwnReact/index.js export { default as createElement} from "./createElement.js" import createElement from "./createElement" export default { createElement } Copy code

render function

Next, we should write our render function

In fact, the role of the render function is to help us

createElement
The created object is rendered into real dom

Before that, we need to write a

utils.js
To provide us with some tools and methods

///myOwnReact/utils.js export function checkIsTextNode ( node ) { //This method is used to check whether a node created by createElement is a text node return node.type === "text" ; } Copy code
///myOwnReact/render.js import {checkIsTextNode} from "./utils.js" ; //The render method accepts two parameters: //1. element: the element object created by createElement //2. container: the real dom container function render ( element, container ) { const isTextNode = checkIsTextNode(element); //1. we need to create a real node by the type of the root element. //But we need to distinguish the node type. If it is a text node, we should not create an element. const rootDom = isTextNode? Document .createTextNode( "" ): document .createElement (element.type); //Here we still have to distinguish between text nodes and dom nodes, because text nodes do not have a setAttribute method. //We need to give the text node directly to the nodeValue of the text node //Of course, when we created the text node at the beginning, you You can pass in nodeValue as a parameter. //It s just that we passed an empty string above. This depends on personal preference if (isTextNode) { rootDom.nodeValue = props.nodeValue; } else { //If it is a dom node, we need to add all props to the real rootDom, but except for children const {children = [], ...restProps} = element.props; const attrs = Object .keys( restProps); attrs.forEach( k => domElement.setAttribute(k, restProps[k])); //Recursive child elements children.forEach( child => render(child, domElement)); } container.appendChild(rootDom) } Copy code

So far our render method is finished

Similarly we need the above

index.js
Named export

///myOwnReact/index.js ... export { default as render} from "./render.js" import render from "./render.js" export { ..., render } ... Copy code

At this point, we can create a

index.html
, And then write the following code, we can see if the result we want appears on the page

<!DOCTYPE html > < html lang = "en" > < head > < meta charset = "UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < title > Build Ur Own React </title > </head > < body > < div id = "root" > </div > <script type = "module" > import {createElement, render} from "./index.js" const element = createElement( "div" , { class : "wrapper" }, createElement( "span" , { class : "title" }, "I am the title" ), createElement( "div" , { class : "content" }, "I am the content" )); console .log( "element" , element); render(element, document .getElementById( "root" )); </script > </body > </html > Copy code

turn on

live server
, We can see the effect of the page and the browser
element
The print result is as follows:

We can see that our label has been successfully rendered in the html document, and at the same time, browsing the console also printed out the node we created recursively.

Concurrent Mode & Fiber

Let s explore a question: When the above

render
After the method starts executing, we can interrupt
render
Implementation of the method?

Obviously, the answer is no, so what kind of problem will this cause?

Once we start this "rendering" operation, we cannot interrupt the rendering before the complete real dom tree is rendered. Then if our virtual dom tree is particularly large, then the corresponding rendering into the real dom tree will take more time Long, we all know that JS is single-threaded, so this rendering operation will always be performed in the main thread and block subsequent tasks. Let's assume there is a scene where we initially rendered an input box, and then this time There are still a lot of things that need to be rendered and rendered for more than ten seconds. In these ten seconds, after the user enters something in the input box, although he can be found by the event listener thread, will the main thread dump him? Yes, so here is the problem

To solve this problem, React's idea is this:

  • Decompose the entire rendering process into n multiple small units
  • After each small unit is rendered, let's see if there is interaction now, and if there are other operations that need to interrupt the rendering, if so, then interrupt the rendering, if not, proceed to the next unit rendering

We need to remodel our

render.js
file

We are now directly rendering in the render method, and directly recursively rendering all of his child nodes as soon as we render. This is definitely not OK. According to our above statement, we need to render a huge The work is split into small and independent UI units, so that rendering is easier

Updating at the same time

render.js
Before this file, I suggest you to check it out
requestIdleCallback
This function: developer.mozilla.org/zh-CN/docs/...

Simply put, this function will help you calculate whether there are currently high-priority tasks that need to be performed in the browser stack (such as user input and animation response, etc.), we can directly use this function to assist us in completing user input Response to high-priority operations

Then let's briefly talk about Fiber. We all know that there is a virtual dom pair in Vue, and there is also a virtual dom in React, but its name is Fiber.

//The object we create through createElement is not a virtual dom, it is just a basic description object //Simply put fiber is a kind of data structure const Fiber = { type : null , //the label type corresponding to the fiber node parent : null , //parent fiber node sibling : null , //brother fiber node child : null , //own fiber node dom : null , //the real dom element corresponding to the fiber node props : {}, //all attributes of the fiber node effectTag : null , //the update status corresponding to the fiber node, in the update phase Will use } //Each attribute has its own purpose, as we write the follow-up you will know copy the code
import {checkIsTextNode} from "./utils.js" ; let nextUnitOfWork = null ; //We assume that what is stored in this variable is the UI unit that needs to be rendered each time, and we constantly change the value of this variable to control what is being rendered this time //We now know that render his task is actually to render an entire dom tree, but we need to change the strategy, we use render to start an automatic scheduling task //This scheduling task will continue to help us with dom The rendering is like a business line on the assembly line, but the scheduling task will stop when there is nothing to render.//At the same time, it will stop when it needs to be stopped (when does it need to be stopped? For example, we said above User high priority input response operation) export default function render ( element, container ) { //We now decide whether to turn on the scheduling switch based on whether there is a value for nextUnitOfWork //So we want to turn on scheduling, then assign nextUnitOfWork = { //nextUnitOfWork is actually a fiber node type : null , dom : container, parent : null , //parent fiber node sibling : null ,//Brother fiber node child : null , //His child fiber node effectTag : "placement" , //This is a fiber tag that will be used in the subsequent update phase, placement means the new node props : { children : [element ] } } } //Then we inevitably need a thing to perceive whether nextUnitOfWork is worthwhile, let's write a workLoop method //What is this deadline: it is a parameter passed to us by requestIdleCallback, we will use this parameter later //Decide whether we need to stop the rendering of the next unit function workLoop ( deadline ) { let shouldYield = false ; //Whether we need to stop rendering a flag, false means no need, true means need to stop while (nextUnitOfWork && !shouldYield) { //As long as the current work object to be processed has a value and the system does not stop us (shouldYield is false), then we will always //perform the task //performUnitOfWork is a function we will make up below nextUnitOfWork = performUnitOfWork(nextUnitOfWork); //Deadline is a parameter about the browser s leisure time. There is a method timeRemaining in it. The call of this method //will return a number of milliseconds, which represents the remaining estimated time of the browser s current idle time, such as when the browser has When the task is coming, he will know //and he will roughly calculate how long it will take for the task to come, so when this time is running out, we need to stop the rendering task, //let the browser do it What he should do shouldYield = deadline.timeRemaining() < 1 ; } //When stopped, the browser will actually have time to respond to user operations, but we still have to remember that we need to continuously turn on monitoring requestIdleCallback(workLoop); } function createDom ( fiber ) { //We will construct the only real dom belonging to the fiber node based on the passed fiber node const isTextNode = checkIsTextNode(fiber); const domElement = isTextNode? document .createTextNode( "" ): document .createElement(fiber.type); //Similarly, if it is a text node, we will directly assign the nodeValue if (isTextNode) domElement.nodeValue = fiber.props.nodeValue; else { //Otherwise, we will assign the props const {children = [], .. .attrs} = fiber.props; const keys = Object .keys(attrs); keys.forEach( k => { domElement.setAttribute(k, attrs[k]); }) //Note: We do not deal with sub-elements here, we only deal with him } return domElement; } function performUnitOfWork ( fiber ) { //There are only a few things we need to do in this method: //1. Create the corresponding real dom node for him according to the current fiber node fiber.dom == null && (fiber.dom = createDom(fiber)) //2. Push the dom of nextUnitOfWork into the parent's dom if (fiber.parent) { //If the fiber node has a parent fiber element, then we can push the fiber to the parent node fiber.parent.dom.appendChild(fiber.dom) } //3. Start to build some sibling nodes and child nodes of the fiber const elements = fiber.props.children; let index = 0 ; let prevFiber = null ; //This is an index entry that we use to maintain the entire fiber linked list //Please note: this elements can all be the description objects created by createElement while (index < elements.length) { let newFiber = { type : elements[index].type, props : elements[index].props, parent : fiber, //Is his parent node the fiber node this time? A child : null , sibling : null , effectTag : "Placement" } //Then in fact, our fiber node does not have a child attribute yet if (index === 0 ) fiber.child = newFiber; else { prevFiber.sibling = newFiber; } prevFiber = newFiber; index ++; } //4. We also need to return the nextUnitOfWork scheduled next time to ensure that a new fiber node can be rendered every time if (fiber.child) return fiber.child; let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.parent; } } //Note: Here we directly open the scheduling task through requestIdleCallback requestIdleCallback(workLoop); Copy code

render phase & commit phase

I don't know if you have considered another issue. We will now split the rendering of the entire UI into many small UI units for progressive rendering. Assuming that the user has an input in the middle, we assume that the JS processing logic of this input is 10 seconds. Then the UI will not continue to render within these 10 seconds, and the user sees a broken UI interface, which is not what we want at all, so we need to change our policy

We don t stuff the dom container one by one now. After we have built the entire fiber tree (which means that there is no JS logic processing at this time), we will plug the entire tree directly into the real dom (relying on the fiber The dom in the node), why did we say that the user's response will not be timely, because we will have a lot of JS logic operations when rendering, and we will stuff the dom into the real container, which consumes far less time There are many above, so we hope this more, so here are two concepts involved:

  • Render phase: represents the stage where we perform JS logic processing and build the entire fiber tree. If there is a user response that must be processed by us at this stage (especially when updating ), then we will stop the current work and go directly to the priority Higher user response and other operations
  • commit phase: It means that our entire fiber tree has been built and is being inserted into the real dom container. At this time, we will not care about user interaction and priority, and the entire process cannot be interrupted.

Then we have to change our

render.js

//1. First of all, every nextUnitOfWork we are now constantly changing, so we can't get the root node at all //And the assignment of nextUnitOfWork in render must be the root node ... let nextUnitOfWork = null ; //we add one more variable let wipRoot = null ;//represents the reference of the root node of the entire fiber tree, because we know that we need to save the reference of a tree to save its root node and it will be OK ... function render ( element, container ) { //we need to update the render method wipRoot = { dom : container, parent : null , sibling : null , child : null , props : { children : [element], }, effectTag : "placement" , type : null } nextUnitOfWork = wipRoot; } ... //In our workLoop method, we can submit the entire fiber tree function workLoop ( deadline ) { ... //Why is it possible in workLoop? Mainly because we know that every change in the value of nextUnitOfWork is actually processed in workLoop //So we can only perceive whether the current fiber tree is already in workLoop Finished //We judge directly below the while loop //If our nextUtilOfWork is null at this time, it means that the entire fiber tree has been constructed, so all we have to do is to directly enter the commit phase if (!nextUnitOfWork && wipRoot) { //Why must nextUnitOfWork be null? The main reason is that even if it is not empty, it may come here. //Because when rendering is interrupted, nextUnitOfWork must still have a value at this time, but he will definitely go In this process, we always only hope for one point, that is, we will submit the entire fiber tree after it has been built. commitRoot(); } //When stopped, the browser will actually have time to respond to user operations, but we still have to remember that we need to continuously turn on monitoring requestIdleCallback(workLoop); ... } //Add the following two methods at the same time, both of which are relatively simple, I won't talk about it. function commitRoot () { commitWork(wipRoot.child); //Because wipRoot must be the container dom, so we directly submit wipRoot = null from the child element ; } function commitWork ( fiber ) { if (!fiber) return ; fiber.parent.dom.appendChild(fiber.dom); commitWork(fiber.child); commitWork(fiber.sibling); } Copy code

Reconciliation

You must have heard of it

Vue
one of
diff
Algorithm, also as an MVVM framework,
React
It also has its own internal comparison algorithm for virtual dom, which is called
Reconciliation

The main process is that we need to compare the last saved virtual dom tree every time we update, so as to determine which nodes we have updated, which nodes have been deleted, and which new nodes have been added.

We are again to our

render.js
File started

//1. 1. we add a currentRoot globally to save the previous fiber tree, and at the same time add a deleteGroup to save the dom elements that need to be deleted in this comparison ... let nextUnitOfWork = null ; let wipRoot = null ; let currentRoot = null ; //The entire fiber Tree we want to save this time let deleteGroup = []; //The collection of deleted fibers //Then we need to add some code to commitRoot function commitRoot () { //Because we will clear wipRoot here, we must save the reference before clearing it ... currentRoot = wipRoot; wipRoot = null ; ... } ... //Then we have to update our performUnitOfWork function function performUnitOfWork ( fiber ) { ... //2. Start building some sibling nodes of the fiber, the relationship between the child nodes const elements = fiber.props.children; //We wrote a large piece of stuff to deal with the child node fiber here, we don't need it, just take it out //the code looks weird reconciliationChildren(fiber, elements); ... } //Then write our reconciliationChildren method //We define a method for diff comparison of child elements function reconciliateChildren ( wipRoot, elements ) { //1. First we get a recently saved virtual dom tree const oldFiber = wipRoot.alternate && wipRoot.alternate.child; let index = 0 ; let prevFiber = null ; //This is an index entry that we use to maintain the entire fiber linked list //Please note: the elements in this element are all description objects created by createElement //Because we have to compare layer by layer here, and will modify the value of oldFiber multiple times, we cannot end with //index <elements.length, because if elements.length is gone //but oldFiber and that actually represent the delete operation in the latest fiber node while (index <elements.length || oldFiber != null ) { const el = elements[index]; //If the oldFiber and the new el type are the same this time, we will save some information to save performance const isSameType = el && oldFiber && el.type === oldFiber.type; let newFiber = null ; if (isSameType) { //represents the update stage newFiber = { type : oldFiber.type, parent : wipRoot, sibling : null , child : null , props : el.props, alternate : oldFiber, effectTag : "update" , dom : oldFiber.dom } } else if (oldFiber && !isSameType) { //represents deletion oldFiber.effectTag = "delete" ; deleteGroup.push(oldFiber); //Add an oldFiber to the collection that was deleted this time } else if (el && !isSameType) { //represents a new addition newFiber = { type : elements[index].type, props : elements[index].props, parent : wipRoot, //Is his parent node the fiber node this time, please check this carefully child : null , sibling : null , effectTag : "placement" , dom : null } } //In fact, our fiber node does not have a child attribute yet if (index === 0 ) wipRoot.child = newFiber; else { prevFiber.sibling = newFiber; } prevFiber = newFiber; index ++; } } //After comparing the virtual dom with the reconciliationChildren method above, we actually need to distinguish the status when we commitWork function commitWork ( fiber ) { if (!fiber) return ; if (fiber.effectTag === "placement " ) { //add fiber.parent && fiber.parent.dom.appendChild(fiber.dom); } else if (fiber.effectTag === "update" ) { updateDom(dom, fiber.alternate.props, fiber.props, ); } else if (fiber.effectTag === "delete" ) { fiber.parent.dom.removeChild(fiber.dom); } commitWork(fiber.child); commitWork(fiber.sibling); } function updateDom ( dom, prevProps, nextProps ) { //1. The first thing I want to see is whether there are any removed properties const withoutChildrenPrevProps = Object .keys(prevProps).filter( k => k !== "children" ) ; const withoutChildrenNextProps = Object .keys(nextProps).filter( k => k !== "children" ); //What I have to do is to traverse all the old properties, and if the old ones are missing, the new ones will be removed. //Otherwise, they will be updated withoutChildrenPrevProps.forEach( k => { if (k.startsWith( "on" )) { //This means it is an event, the event has to be relaxed const legalEventName = k.toLowerCase().substring( 2 ); //We know that events are marked in onClick in React, we only need lowercase click //The event is actually divided into removal or update if (!(k in withoutChildrenNextProps)) { //It means there is nothing, why should I keep it? dom.removeEventListener(legalEventName, prevProps[k]); } else { //Direct binding dom.addEventListener(legalEventName, nextProps[k]); } } else if (!(k in withoutChildrenNextProps)) { //If there is no such key in the new property, just bye bye dom[k] = "" ; } else { //This must be the update stage, all of which are based on new ones, of course you can also perform in-depth optimization comparisons dom[k] = nextProps[k]; } }) } Copy code

So far, our reconciliation phase has also been completed

Our final render.js file code is as follows:

import {checkIsTextNode} from "./utils.js" ; let nextUnitOfWork = null ; //We assume that what is stored in this variable is the UI unit that needs to be rendered each time, and we constantly change the value of this variable to control what is rendered this time let wipRoot = null ; //represents my last The entire fiber tree to be submitted let currentRoot = null ; // The entire fiber tree we want to save this time let deleteGroup = []; //The deleted fiber collection //We now know that render his task is actually to render an entire dom tree, but we need to change the strategy. We use render to start an automatic scheduling task //This scheduling task will continue to help us with dom The rendering is like a business line on the pipeline, but the scheduled task will stop when there is nothing to render. //At the same time, it will stop when it needs to be stopped (when does it need to be stopped? For example, we said above User high priority input response operation) export default function render ( element, container ) { //We now decide whether to turn on the scheduling switch based on whether there is a value for nextUnitOfWork //So we want to turn on scheduling, then assign nextUnitOfWork to wipRoot = { //nextUnitOfWork is actually a fiber node type : null , dom : container, parent : null , //parent fiber node sibling : null ,//brother fiber node child : null , //His child fiber node effectTag : "placement" , //This is a fiber tag that will be used in the subsequent update phase, placement means the new node props : { children : [element ] }, alternate : currentRoot, //we also save our last fiber tree at the root node } deleteGroup = []; //We should empty the deleted fiber array every time we render nextUnitOfWork = wipRoot; } //Then we inevitably need a thing to perceive whether nextUnitOfWork is worthwhile, let's write a workLoop method //What is this deadline: it is a parameter passed to us by requestIdleCallback, we will use this parameter later //Decide whether we need to stop the rendering of the next unit function workLoop ( deadline ) { let shouldYield = false ; //do we need to stop rendering a flag, false means no need, true means need to stop while (nextUnitOfWork && !shouldYield) { //as long as the current work object to be processed has value and the system does not let us Stop (shouldYield is false), then we will always //perform the task //performUnitOfWork is a function we will make up below nextUnitOfWork = performUnitOfWork(nextUnitOfWork); //Deadline is a parameter about the browser s leisure time. There is a method timeRemaining in it. The call of this method //will return a number of milliseconds, which represents the remaining estimated time of the browser s current idle time, such as when the browser has When the task is coming, he will know //and he will roughly calculate how long it will take for the task to come, so when this time is running out, we need to stop the rendering task, //let the browser do it What he should do shouldYield = deadline.timeRemaining() < 1 ; } //If our nextUtilOfWork is null at this time, it means that the entire fiber tree has been constructed, so what we have to do is directly enter the commit phase if (!nextUnitOfWork && wipRoot) { //Why must nextUnitOfWork be null? Ha, it s mainly because even if it s not empty, it might come here.//Because when rendering is interrupted, nextUnitOfWork must still have a value at this time, but it will definitely enter this process.// We always only hope that, That is, the entire fiber tree is confirmed to be completed and we will submit it commitRoot(); } //When stopped, the browser will actually have time to respond to user operations, but we still have to remember that we need to continuously turn on monitoring requestIdleCallback(workLoop); } function commitRoot () { deleteGroup.forEach(commitWork); //see if there are any deleted things commitWork(wipRoot.child); //because wipRoot must be the container dom, so we directly start submitting from the child element //What can't run away must be a retention of a virtual dom tree this time in the commit phase currentRoot = wipRoot; wipRoot = null ; } function commitWork ( fiber ) { if (!fiber) return ; if (fiber.effectTag === "placement" ) { //new fiber.parent && fiber.parent.dom.appendChild(fiber.dom); } else if (fiber.effectTag === "update" ) { updateDom(dom, fiber.alternate.props, fiber.props, ); } else if (fiber.effectTag === "delete" ) { fiber.parent.dom.removeChild(fiber.dom); } commitWork(fiber.child); commitWork(fiber.sibling); } function updateDom ( dom, prevProps, nextProps ) { //1. The first thing I want to see is whether there are any removed properties const withoutChildrenPrevProps = Object .keys(prevProps).filter( k => k !== "children" ) ; const withoutChildrenNextProps = Object .keys(nextProps).filter( k => k !== "children" ); //What I have to do is to traverse all the old properties, and if the old ones are missing, the new ones will be removed. //Otherwise, they will be updated withoutChildrenPrevProps.forEach( k => { if (k.startsWith( "on" )) { //This means it is an event, the event has to be relaxed const legalEventName = k.toLowerCase().substring( 2 ); //We know that events are marked in onClick in React, we only need lowercase click //The event is actually divided into removal or update if (!(k in withoutChildrenNextProps)) { //It means there is nothing, why should I keep it? dom.removeEventListener(legalEventName, prevProps[k]); } else { //Direct binding dom.addEventListener(legalEventName, nextProps[k]); } } else if (!(k in withoutChildrenNextProps)) { //If there is no such key in the new property, just bye bye dom[k] = "" ; } else { //This must be the update stage, all of which are based on new ones, of course you can also perform in-depth optimization comparisons dom[k] = nextProps[k]; } }) } function createDom ( fiber ) { //We will construct the only real dom belonging to the fiber node based on the passed fiber node const isTextNode = checkIsTextNode(fiber); const domElement = isTextNode? document .createTextNode( "" ): document .createElement(fiber.type); //Similarly, if it is a text node, we will directly assign the nodeValue if (isTextNode) domElement.nodeValue = fiber.props.nodeValue; else { //Otherwise, we will assign the props const {children = [], .. .attrs} = fiber.props; const keys = Object .keys(attrs); keys.forEach( k => { domElement.setAttribute(k, attrs[k]); }) //Note: We do not deal with sub-elements here, we only deal with him } return domElement; } function performUnitOfWork ( fiber ) { //There are only a few things we need to do in this method: //1. Create the corresponding real dom node for him according to the current fiber node fiber.dom == null && (fiber.dom = createDom(fiber)) //2. Start building some sibling nodes of the fiber, the relationship between the child nodes const elements = fiber.props.children; reconciliateChildren(fiber, elements); //3. We also need to return the nextUnitOfWork scheduled next time to ensure that a new fiber node can be rendered every time if (fiber.child) return fiber.child; let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.parent; } } //We define a method for diff comparison of child elements function reconciliateChildren ( wipRoot, elements ) { //1. First we get a recently saved virtual dom tree const oldFiber = wipRoot.alternate && wipRoot.alternate.child; let index = 0 ; let prevFiber = null ; //This is an index entry that we use to maintain the entire fiber linked list //Please note: the elements in this element are all description objects created by createElement //Because we have to compare layer by layer here, and will modify the value of oldFiber multiple times, we cannot end with //index <elements.length, because if elements.length is gone //but oldFiber and that actually represent the delete operation in the latest fiber node while (index <elements.length || oldFiber != null ) { const el = elements[index]; //If the oldFiber and the new el type are the same this time, we will save some information to save performance const isSameType = el && oldFiber && el.type === oldFiber.type; let newFiber = null ; if (isSameType) { //represents the update stage newFiber = { type : oldFiber.type, parent : wipRoot, sibling : null , child : null , props : el.props, alternate : oldFiber, effectTag : "update" , dom : oldFiber.dom } } else if (oldFiber && !isSameType) { //represents deletion oldFiber.effectTag = "delete" ; deleteGroup.push(oldFiber); //Add an oldFiber to the collection that was deleted this time } else if (el && !isSameType) { //represents a new addition newFiber = { type : elements[index].type, props : elements[index].props, parent : wipRoot, //Is his parent node the fiber node this time, please check this carefully child : null , sibling : null , effectTag : "placement" , dom : null } } //In fact, our fiber node does not have a child attribute yet if (index === 0 ) wipRoot.child = newFiber; else { prevFiber.sibling = newFiber; } prevFiber = newFiber; index ++; } } //Note: Here we directly open the scheduling task through requestIdleCallback requestIdleCallback(workLoop); Copy code

ok, you have basically realized a small react ecology, from

createElement
To
render
To
fiber
, And then
render phase & commit phase
, And then
reconciliation
You can think about how to deal with functional components and class components. Next time I will publish a blog to talk about them. I hope this blog can be helpful to you.