One, foreword

This article is based on https://pomb.us/build-your-own-react/ to achieve a simple version of React.

B station -React source code, you are in the first layer.

The simulated version is React 16.8.

The following functions will be implemented:

  1. CreateElement (virtual DOM);
  2. Render.
  3. Interruptible rendering;
  4. Fibers;
  5. Render and Commit Phases;
  6. Coordination (Diff algorithm);
  7. Function component;
  8. Hooks;

Now for dinner, read on.

2, preparation,

1. React Demo

Let’s start with a simple React Demo that looks like this:

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

See the complete source code for this example:
reactDemo

Open reactdemo.html in your browser and it looks like this:

We need to implement our React, so we need to know what the code above does.

1.1 element

Const Element =

123

is actually a JSX syntax.

React explains the JSX as follows:

JSX is a JavaScript syntax extension. It is similar to a template language, but it has the full power of JavaScript. JSX will eventually be compiled by Babel as a function call called react.createElement ().

Compile const Element =

123

online with Babel.

Const element =

123
const element = React.createElement("div", {
  title: "foo"
}, "hello");

Let’s see what the React. CreateElement object actually looks like.

Try printing in demo:

const element = <div title="foo">hello</div> console.log(element) const container = document.getElementById('container')  ReactDOM.render(element, container);

You can see the output element as follows:

To simplify element:

const element = {
    type: 'div',
    props: {
        title: 'foo',
        children: 'hello'
    }
}

To summarize, React. CreateElement actually generates an Element object with the following properties:

  • Type: the tag name
  • props

    • Title: indicates the tag attribute
    • Children: the child nodes

1.2 render

Reactdom.render () adds the element to the DOM node with the ID container. Next we’ll simply write a method instead of reactdom.render ().

  1. Create a node with the tag element.
const node = document.createElement(element.type)
  1. Set the title of the node to element.props.

    node["title"] = element.props.title
  2. Create an empty text node text;

    const text = document.createTextNode("")
  3. Set the nodeValue of the text node to element.props. Children;

    text["nodeValue"] = element.props.children
  4. Add text node to node;

    node.appendChild(text)
  5. Add node to Container

    container.appendChild(node)

See the complete source code for this example:
reactDemo2

Run the source code, and the result is the same as when introducing React:

Three,

Above, we simply replaced React. CreateElement and Reactdom. render methods by simulating React, and then we will really start to implement various functions of React.

1. CreateElement (virtual DOM)

CreateElement creates an Element object with the following structure:

// const element = {type: 'div', // tag name props: {// node attribute, including children title: 'foo', // title attribute children: 'hello' // child nodes, note: this should actually be an array structure to help us store more children}}

Based on the structure of the element, the createElement function is designed. The code is as follows:

/** * Create a virtual DOM structure * @param {type} tag name * @param {props} property object * @param {children} child node * @return {element} virtual DOM */ function createElement (type, props, ... children) { return { type, props: { ... props, children: children.map(child => typeof child === 'object' ? child : createTextElement(child) ) } } }

When children are non-objects, we should create a textElement element as follows:

Function createTextElement (text) {return {type:} /** * Create text node * @param {text} @element} virtual DOM */ function createTextElement (text) {return {type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } } }

Let’s try this with the following code:

const myReact = {
    createElement
}
const element = myReact.createElement(
  "div",
  { id: "foo" },
  myReact.createElement("a", null, "bar"),
  myReact.createElement("b")
)
console.log(element)

See the complete source code for this example:
reactDemo3

The resulting Element object looks like this:

const element = {
    "type": "div", 
    "props": {
        "id": "foo", 
        "children": [
            {
                "type": "a", 
                "props": {
                    "children": [
                        {
                            "type": "TEXT_ELEMENT", 
                            "props": {
                                "nodeValue": "bar", 
                                "children": [ ]
                            }
                        }
                    ]
                }
            }, 
            {
                "type": "b", 
                "props": {
                    "children": [ ]
                }
            }
        ]
    }
}

JSX

In fact, when we develop with React, we don’t create components like this:

const element = myReact.createElement(
  "div",
  { id: "foo" },
  myReact.createElement("a", null, "bar"),
  myReact.createElement("b")
)

Instead, through JSX syntax, the code looks like this:

const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

In myReact, we can use JSX syntax by adding comments that tell Babel to translate the function we specify, as follows:

/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

See the complete source code for this example:
reactDemo4

2. render

The render function helps us add the Element to the real node.

It will be achieved in the following steps:

  1. Create a DOM node of type Element. type and add it to the container.
/** * Add virtual DOM to real DOM * @param {element} virtual DOM * @param {container} Real DOM */ function render (element, container) { const dom = document.createElement(element.type) container.appendChild(dom) }
  1. Add both element.children to the DOM node;
element.props.children.forEach(child => 
    render(child, dom)
)
  1. Special processing of text nodes;
const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode("")
    : document.createElement(element.type)
  1. Add the element’s props attribute to the DOM;
const isProperty = key => key ! == "children" Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] })

We have implemented the function of rendering JSX to the real DOM. Let’s try it out. The code is as follows:

const myReact = {
    createElement,
    render
}
/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

myReact.render(element, document.getElementById('container'))

See the complete source code for this example:
reactDemo5

The result is shown in the figure, and the output is successful:

3. Interruptible Rendering (requestIdleCallback)

Let’s see what happens to the child nodes in the render method:

/** * Add virtual DOM to real DOM * @param {element} virtual DOM * @param {container} Real DOM */ function render (element, Container) {/ / government / / traverse all child nodes, and rendered element. The props. Children. The forEach (child = > render (child, dom)) / / omit}

The problem with this recursive call is that once the render starts, all nodes and their children will be rendered before the process ends.

When the DOM tree is large, the page is stuck during the rendering process, and interactive operations such as user input cannot be carried out.

The following steps can be taken to solve the above problems:

  1. Allow to interrupt the rendering work, if there is a higher priority work inserted, then temporarily interrupt the browser rendering, until the completion of the work, resume the browser rendering;
  2. Break down the rendering work into small units;

Use requestIdleCallback to solve the problem of allowing interrupts to render work.

Window. RequestIdleCallback will in the browser’s idle period called function to wait in line. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.

Window. RequestIdleCallback details to view the document:
The document

The code is as follows:

// nextUnitOfWork let nextUnitOfWork = null /** * workLoop @param {deadline} */ function workLoop(deadline) {// Should I stop the work loop function let shouldYield = false // if the nextUnitOfWork exists and there is no other work with higher priority, the loop executes while (nextUnitOfWork &&! ShouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // If the deadline is approaching, Stop working loop function shouldYield = deadline.timeremaining () < 1} // notify the browser, // The idle time should execute workLoop requestIdleCallback(workLoop) // The idle time should execute workLoop requestIdleCallback(workLoop) // The execution unit event, Function performUnitOfWork(nextUnitOfWork) {// TODO}

PerformUnitOfWork is used to execute unit events and return the next unit event, as described below.

4. Fiber

RequestIdleCallback allows the browser to render units of work at idle time, avoiding the problem of overrendering and page stalling.

Note: requestIdleCallback is actually unstable and not recommended for use in production environments. This example is only used to simulate React ideas. React itself does not use requestIdleCallback to render units of work in the browser at idle time.

On the other hand, In order to make the render work separate into small units, React designed Fiber.

Each element is a fiber structure, and each fiber is a rendering unit.

So fiber is both a data structure and a unit of work.

Fiber is described below with a simple example.

Suppose you want to render such an Element tree:

myReact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

The generated fiber tree is shown in figure:

Orange is the child node, yellow is the parent node, and blue is the sibling node.

Each fiber has a link to its first child, its next sibling, and its parent. This data structure makes it easier to find the next unit of work.

The arrow in the image above also shows the rendering process of Fiber, which is described in detail below:

  1. Starting from root, find the first child div;
  2. Find the first child of the div h1;
  3. Find h1’s first child p;
  4. Find the first child node of P, if there is no child node, then find the next sibling node, find the sibling node a of P;
  5. Find the first child node of A, if there is no child node, and no sibling node, then find the next sibling node of its parent node, find the sibling node h2 of a’s parent node;
  6. Look for the first child of h2, can’t find it, look for the sibling, can’t find it, look for the sibling of the parent div, can’t find it, continue to look for the sibling of the parent div, find root;
  7. Step 6: The root node has been found, rendering is complete.

The following rendering process is implemented in code.

  1. Detach the part of Render that creates the DOM node into the creactDOM function;
/** * createDom * @param {fiber} fiber */ function createDom (fiber) {/** * createDom (fiber); Const dom = fiber. Type === 'TEXT_ELEMENT'? document.createTextNode("") : Document.createelement (fiber.type) const isProperty = key => key! == "props"; Object.keys(fiber.props).filter(isProperty).foreach (name => {dom[name] = fiber.props[name]} return dom }
  1. Set the first unit of work in Render as the fiber root node;

The fiber root node only contains the children attribute, and the value is the parameter fiber.

// nextUnitOfWork let nextUnitOfWork = null /** * add fiber to real DOM * @param {element} fiber * @param {container} real DOM */ function render (element, container) { nextUnitOfWork = { dom: container, props: { children: [element] } } }
  1. RequestIdleCallback renders the fiber when the browser is idle;
/** * workLoop * @param {deadline} deadline */ function workLoop(deadline) {// should not stop the workLoop function let shouldYield = false // The loop executes while (nextUnitOfWork &&! ShouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // If the deadline is approaching, Stop working loop function shouldYield = deadline.timeremaining () < 1} // notify the browser, WorkLoop requestIdleCallback(workLoop)} // Notify the browser that workLoop requestIdleCallback(workLoop) should be executed during idle time
  1. Render fiber function performUnitOfWork;
/** * performUnitOfWork * @param {fiber} fiber * @return {nextUnitOfWork} next unit */ function PerformUnitOfWork (fiber) {// TODO adds a DOM node // TODO creates a filber // TODO returns the next unit of work (fiber)}

4.1 Adding a DOM Node

Function performUnitOfWork(fiber) {// create a DOM node for fiber if (! Fibre.dom) {fibre.dom = createDom(fibre.dom)} The fiber. The dom is added to the parent node if (fiber. The parent) {fiber. The parent. The dom. The appendChild (dom) fiber.}}

4.2 new filber

Function performUnitOfWork(fiber) {// ~ ~ ellipses ~ ~ // const elements = fiber.props. Children // Let index = 0 // While (index < elements. Length) {const element = elements[index] // Create fiber const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null, If (index === 0) {fiber.child = newFiber} else if (element) {// Set the first child node to the sibling node of that node prevSibling.sibling = newFiber } prevSibling = newFiber index++ } }

4.3 Return to the next unit of Work (fiber)

Function performUnitOfWork(fiber) {function performUnitOfWork(fiber) { If (fiber.child) {return fiber.child} let nextFiber = fiber while (nextFiber) {// if there are siblings, If (nextfiber.sibling) {return nextfiber.sibling} // Otherwise continue the while loop until root is found. nextFiber = nextFiber.parent } }

We have implemented the ability to render fiber to the page, and the rendering process is interruptible.

Now try it out. The code looks like this:

const element = (
    <div>
        <h1>
        <p />
        <a />
        </h1>
        <h2 />
    </div>
)

myReact.render(element, document.getElementById('container'))

See the complete source code for this example:
reactDemo7

Output dom as expected, as shown in the figure below:

5. Render commit phase

Since we made the rendering process interruptible, we certainly didn’t want the browser to show the user a half-rendered UI.

The optimization of the render commit phase is as follows:

  1. Remove the logic in performUnitOfWork about adding children to parent nodes;
Function performUnitOfWork (fiber) {/ / had deleted the if (fiber. The parent) {fiber. The parent. The dom. The appendChild (dom) fiber.}}
  1. Add a root variable to store the fiber root node.
Let wipRoot = null function render (element, container) {wipRoot = {dom: container, props: {children: [element]}} nextUnitOfWork is the root node nextUnitOfWork = wipRoot}
  1. When all fibers work, nextUnitOfWork is undefined and the real DOM is rendered.
Function workLoop (deadline) {if (! NextUnitOfWork && wipRoot) {commitRoot()}
  1. New commitRoot function (commitRoot) to render the fiber tree to a real DOM recursion.
// Render the fiber tree as a real DOM; Function commitRoot () {commitWork(wiproot.child) // Set this parameter to NULL, otherwise workLoop will continue to execute when the browser is idle. WipRoot = null} /** * performUnitOfWork * @param {fiber} fiber */ function commitWork (if (! Fiber) return const domParent = fiber.parent-dom domparent-.appendChild (fiber.dom) // commitWork(fiber.child) // CommitWork (fiber.sibling)}

See the complete source code for this example:
reactDemo8

The running results of the source code are as follows:

6. Coordination (Diff algorithm)

When element is updated, the fiber tree before update needs to be compared with the fiber tree after update. After the comparison result is obtained, only the DOM node corresponding to the changed fiber is updated.

Through coordination, the number of operations on the real DOM is reduced.

1. currentRoot

Add currentRoot variable to save the alternate tree before the root node is updated. Add alternate attribute for fiber to save the alternate tree before the root node is updated.

Let currentRoot = null function render (element, container) {wipRoot = {// omit alternate: currentRoot } } function commitRoot () { commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null }

2. performUnitOfWork

The new fiber logic in performUnitOfWork is extracted to reconcileChildren function.

/** * reconcileChildren * @param {fiber} fiber */ Function reconcileChildren (fiber, fiber, reconcileChildren) Elements) {let prevSibling = null let prevSibling = null while (index < elements.length) { Const newFiber = {type: element.type, props: element.props, parent: fiber, dom: If (index === 0) {fiber. Child = newFiber} else if (element) {// other children of fiber, Sibling = newSibling} // Assign newFiber to prevSibling, PrevSibling = newFiber // index+ 1 index++}}

3. reconcileChildren

Comparing old and new fibers in reconcileChildren;

3.1 When old and new fiber types are the same

Keep the DOM, UPDATE props only, and set the effectTag to UPDATE;

function reconcileChildren (wipFiber, {// oldFiber = wipFiber. Alternate && wipFiber.alternate.child while (index < elements.length || oldFiber ! = null) {const element = elements[index] let newFiber = null // fiber type is the same const sameType = oldelement && If (sameType) {newFiber = {type: oldFiber. Type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE",}} // ~ ~ omit ~ ~} // ~ ~ omit ~ ~}

3.2 When old and new fiber types are different and there are new elements

Create a new DOM node and set the effectTag to PLACEMENT;

ReconcileChildren (wipFiber, elements) {// ~ ~ omit ~ ~ if (element &&! sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: Null, effectTag: "PLACEMENT",}} // ~ ~ omit ~ ~}

3.3 When the types of old and new fibers are different and there are old fibers

Delete the old fiber and set the effectTag to DELETION.

Function reconcileChildren (wipFiber, elements) {// ~ ~ omit ~ ~ if (oldFiber &&! SameType) {oldFiber. EffectTag = "DELETION" deletions.push(oldFiber)}

4. deletions

Create a deletions array to store the fiber node to be deleted. When rendering DOM, traverse the deletions to delete the old fiber node.

Let deletions = null function render (element, container) { Deletions = []} // When rendering DOM, Function commitRoot () {deletions.foreach (commitWork)}

5. commitWork

The effectTag of fiber is determined in commitWork and processed separately.

5.1 PLACEMENT

When the effectTag of fiber is PLACEMENT, it means that the fiber is added and the node is added to the parent node.

if ( fiber.effectTag === "PLACEMENT" && fiber.dom ! = null ) { domParent.appendChild(fiber.dom) }

5.2 DELETION

When the effectTag of fiber is DELETION, fiber is deleted and the parent node is deleted.

else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
}

The 5.3 UPDATE

When the effectTag of fiber is set to UPDATE, it means to UPDATE fiber and UPDATE the props property.

else if (fiber.effectTag === 'UPDATE' && fiber.dom ! = null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) }

The updateDom function updates the props property based on the update type.

const isProperty = key => key ! Const isNew = (prev, next) => key => prev[key]! Const isGone = (prev, next) => key =>! (key in next) function updateDom(dom, prevProps, NextProps) {// delete the old attribute object.keys (prevProps).filter(isProperty).filter(isGone(prevProps, NextProps).foreach (name => {dom[name] = ""}) // Update object.keys (nextProps).filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) }

In addition, add update and delete event properties to updateDom to make it easier to track updates to fiber events.

function updateDom(dom, prevProps, NextProps) {// ~ ~ omit ~ ~ const isEvent = key => key.startswith ("on") // Delete the old or changed event object.keys (prevProps) .filter(isEvent) .filter( key => ! (key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.removeEventListener( EventType, prevProps[name])}) object.keys (nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, NextProps [name])}) // ~ ~ omit ~ ~}

Replace the logic for setting props in creactDOM.

function createDom (fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? Document.createtextnode ("") : document.createElement(fiber.type) updateDom(dom, {}, fiber.props) return dom}

Create a new example with input form items and try updating Element with the following code:

/** @jsx myReact.createElement */
const container = document.getElementById("container")

const updateValue = e => {
    rerender(e.target.value)
}

const rerender = value => {
    const element = (
        <div>
            <input onInput={updateValue} value={value} />
            <h2>Hello {value}</h2>
        </div>
    )
    myReact.render(element, container)
}

rerender("World")

See the complete source code for this example:
reactDemo9

The output results are as follows:

7. Functional components

Let’s start with a simple example of a functional component:

MyReact does not yet support functional components. The following code will report an error when running, but this is only used as a comparison to how functional components are normally used.

/** @jsx myReact.createElement */ const container = document.getElementById("container") function App (props) { return (  <h1>hi~ {props.name}</h1> ) } const element = ( <App name='foo' /> ) myReact.render(element, container)

Functional components differ from HTML tag components in two ways:

  • The fiber function component has no DOM node;
  • The children of the function component needs to be obtained after running the function;

The function component is implemented by the following steps:

  1. Modify performUnitOfWork to execute a fiber unit of work based on the fiber type.
Function performUnitOfWork(fiber) {const isFunctionComponent = fiber && fiber.type && fiber.type Instanceof Function // If it is a Function component, If (isFunctionComponent) {updateFunctionComponent(fiber)} else {// If it is not a function component, UpdateHostComponent (fiber)}
  1. Define the updateHostComponent function to execute non-functional components;

Nonfunctional components can pass fiber.props. Children directly as a parameter.

function updateHostComponent(fiber) { if (! fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }
  1. Define updateFunctionComponent and execute the function component.

The function component needs to run to get fiber.children.

Function updateFunctionComponent(fiber-type) {// fiber-type = fiber-type; Const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children) for example
  1. Modify the commitWork function to accommodate fibers without DOM nodes.

4.1 Modify the domParent acquisition logic to keep searching upward through the while loop until the parent fiber with DOM nodes is found;

Function commitWork (commitWork (commitWork)) {let domParentFiber = fiber.parent Continue to look for fiber.parent-parent-dom until you have a DOM node. while (! Domparentfiber.dom) {domParentFiber = domparentfiber.parent} const domParent = domparentfiber.dom // omit}

4.2 Modify the logic of node deletion. When deleting a node, it is necessary to keep looking down until the sub-fiber with DOM node is found;

Function commitWork (fiber) {function commitWork (fiber) { CommitDeletion else if (fiber.effectTag === "DELETION") {commitDeletion(fiber.dom, Function commitDeletion (fiber, domParent) {// deletion (domParent, domParent); // deletion (domParent, domParent); If (fibre.dom) {domparent-removechild (fibre.dom)} else {// if (fibre.dom) {domparent-removechild (fibre.dom)} else {// if (fibre.dom) { CommitDeletion (fiber.child, domParent)}}

Let’s take a look at the above example. The code is as follows:

/** @jsx myReact.createElement */ const container = document.getElementById("container") function App (props) { return (  <h1>hi~ {props.name}</h1> ) } const element = ( <App name='foo' /> ) myReact.render(element, container)

See the complete source code for this example:
reactDemo10

The running results are as follows:

8. hooks

Let’s continue to add the ability to manage state to myReact. The expectation is that function components have their own state and can retrieve and update state.

A function component with a count function is as follows:

function Counter() {
    const [state, setState] = myReact.useState(1)
    return (
        <h1 onClick={() => setState(c => c + 1)}>
        Count: {state}
        </h1>
    )
}
const element = <Counter />

It is known that a useState method is required to retrieve and update the status.

Again, the rendering of the function component is based on the execution of the function component, so if the above Counter wants to update the count, it will execute the Counter function once for each update.

Through the following steps:

  1. New global variable wipFiber;
Function updateFunctionComponent(fiber-fiber) {fiber-fiber (fiber-fiber) {fiber-fiber (fiber-fiber) Wipfiber.hook = [] // omit}
  1. New useState function;
Function useState (initial) {// if there is an old hook, Const oldHook = wipfiber.alternate && wipfiber.alternate. Hook // initialize hook, Const hook = {state: oldHook? State: initial, queue: [],} // get all the actions from the oldHook queue and apply them to the new hook state const actions = oldHook? oldHook.queue : [] actions.foreach (action => {hook.state = action(hook.state)}) const setState = action => {// Add action to hook queue WipRoot = {dom: currentroot. dom, props: currentroot. props, alternate: currentRoot, } nextUnitOfWork = wipRoot deletions = []} // Add the hook to the unit of work wipFiber. Hook = hook // return [hook.state, setState] }

Let’s run the count component as follows:

function Counter() {
    const [state, setState] = myReact.useState(1)
    return (
        <h1 onClick={() => setState(c => c + 1)}>
        Count: {state}
        </h1>
    )
}
const element = <Counter />

See the complete source code for this example:
reactDemo11

The running results are as follows:

This section simply implements the myReact hooks functionality.

After all, react has many implementations worth studying and studying. I hope there will be more functions of React in the next installment.

conclusion

This paper refers to pomB.us for learning, and implements the custom React including virtual DOM, Fiber, Diff algorithm, functional components, hooks and other functions.

In the process of implementation, the editor has a general grasp of the basic terms and implementation ideas of React. Pomb. us is very suitable for beginners to learn materials, you can directly learn through pomB. us, but also recommended to follow this article step by step to realize the common functions of React.

Source code: Github source code.

Suggest following step by step knock, carry on practical practice.

I hope it can help you, thanks for reading ~

Don’t forget to encourage me with a thumbs-up, pen refill ❤️

The resources

  • https://pomb.us/build-your-own-react/
  • Casson – B station -React source code, what floor are you on
  • Write a simple React by hand

    Welcome to the Bump Lab blog: aotu.io

Or follow the concave-convex laboratory public number (AOTULabs), not regular push articles.