[React advanced-3] realize a React from zero

Posted by Ausx on Mon, 21 Feb 2022 08:57:38 +0100

This article introduces how we can implement a framework similar to React by ourselves. Of course, we will not implement all the contents of React, but only realize the basic functions of React, so that we can have a deeper understanding of React. Because of the limited space, I divided this article into upper and lower parts. This article is the next one.

Write in front

This article continues from the previous section to introduce the remaining knowledge, as follows:

  • Render and Commit phases
  • Reconciliation process
  • Function component
  • Hooks

We will introduce these knowledge points in turn in the next part.

Code acquisition

All the codes involved in this article are uploaded to the code cloud. Please get them at the following address:

https://github.com/xuqwCloud/zerocreate_react

Render and Commit phases

In fact, there is a problem in the code we completed before. In workLook(), every time we call the performnunitofwork() method in a loop, we will add a new dom element to the fiber parent node, as shown in the following code:

We also mentioned earlier that since react introduced fiber, our rendering task will be divided into several small task units. After each completion of these small task units, if there are tasks with high priority, the browser will interrupt the execution of these task units and execute tasks with high priority, After execution, we will come back and continue to execute these small task units from scratch. Therefore, in the process of browser interruption, we sometimes see blank and incomplete page rendering on the front-end page, so let's optimize the code before us.

Let's first delete the code that adds a new dom element to the fiber in the performinutofwork () method, as follows:

Then, in the render() method, we give root fiber an alias called wipRoot, and then assign it to nextUnitOfWork. The code is as follows:

let nextUnitOfWork = null;
let wipRoot = null;

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        }
    };
    nextUnitOfWork = wipRoot;
}

Next, if the "next task unit" does not point to anything, it means that we have completed all the work. Therefore, at this time, we submit the whole fiber tree to DOM, which is a brief introduction to the rendering and submission stage. The code is as follows:

let nextUnitOfWork = null;
let wipRoot = null;

function commitRoot() {

}

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        }
    };
    nextUnitOfWork = wipRoot;
}
function workLoop(deadline) {
    let shouldYield = false;
    while(nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }

    if(!nextUnitOfWork && wipRoot) {
        commitRoot();
    }

    requestIdleCallback(workLoop);
}

Next, improve the commitRoot() method. Here we recursively add all elements to the dom. The code is as follows:

function commitRoot() {
    commitWork(wipRoot.child);
    wipRoot = null;
}

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    const domParent = fiber.parent.dom;
    domParent.appendChild(fiber.dom);
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

In this way, we will optimize the situation described at the beginning. Finally, we will recursively add a whole fiber tree to the dom, so this avoids the problem of incomplete pages caused by browser interruption in rendering.

Reconciliation process

So far, we have only implemented the processes of rendering and adding DOM elements. What should we do if our elements need to be deleted and updated? This is what we will introduce next, that is, the reconciliation process. In this process, we need to compare two fiber trees: the new fiber tree received by the render() method and the old fiber tree we finally submitted to the dom.

So before we start, we need a reference to store the last submitted fiber tree, and add the alternate attribute for each fiber element to connect to the old fiber. The code is as follows:

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;

function commitRoot() {
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}
function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
}

Next, we extract the code fragment of creating a new fiber in the performnunitofwork() method into a new function called reconcileChildren() method. The codes in the last two methods are as follows:

function performUnitOfWork(fiber) {
    if(!fiber.dom) {
        fiber.dom = createDom(fiber);
    }

    // if(fiber.parent) {
    //     fiber.parent.dom.appendChild(fiber.dom);
    // }

    const elements = fiber.props.children;
    reconcileChildren(fiber, elements);

    if(fiber.child) {
        return fiber.child;
    }
    let nextFiber = fiber;
    while(nextFiber) {
        if(nextFiber.sibling) {
            return nextFiber.sibling;
        }
        nextFiber = nextFiber.parent;
    }
} 
function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let prevSibling = null;

    while(index < elements.length) {
        const element = elements[index];
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        if(index == 0) {
            wipFiber.child = newFiber;
        }else {
            prevSibling.sibling = newFiber;
        }

        prevSibling = newFiber;
        index++;
    }
}

The reconcileChildren() method implements the reconciliation process of old fiber and new elements. However, our current reconcileChildren() method cannot run directly here, and we need to optimize it next.

We traverse the children(wipFiber.alternate) of the old fiber and the array of elements to be coordinated at the same time. In this process, after ignoring some other information, we only care about oldFiber and element. Element is the element we want to add to the DOM, and oldFiber is the last fiber we submitted for rendering. Through comparison, we can know whether we need to change the DOM, so the code in the reconcileChildren() method can be temporarily optimized as follows:

function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;

    while(index < elements.length || oldFiber != null) {
        const element = elements[index];
        let newFiber = null;

        //Compare old fiber and new element

        if(oldFiber) {
            oldFiber = oldFiber.sibling;
        }
    }
}

There is no comparison process in the above code, so let's add the code fragments of comparison according to the following rules:

  • If the old fiber and the new element have the same type, we only need to update the dom with the new attribute;
  • If the type is different, it means that it is a new element, so we need to add this new dom node;
  • If the type is different and it is an old fiber, we need to delete the old dom node.

According to the above rules, we write the code as follows:

function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;

    while(index < elements.length || oldFiber != null) {
        const element = elements[index];
        let newFiber = null;

        const sameType = oldFiber && element && element.type === oldFiber.type;
        if(sameType) {
            //Update dom
        }
        if(element && !sameType) {
            //Add dom
        }
        if(oldFiber && !sameType) {
            //Delete dom
        }

        if(oldFiber) {
            oldFiber = oldFiber.sibling;
        }
    }
}

In the above process, key is also used in react to have a better reconciliation process, but we will not introduce it in this article for simplicity.

For the dom node to be updated, we can do this: create a new fiber through the old fiber, assign its props attribute to the new element element, and add an effectTag attribute to the new fiber. We will use it in the later submission stage. The code is as follows:

if(sameType) {
    //Update dom
    newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
    }
}

For the case of adding dom, it is more similar to the above. Look directly at the code:

if(element && !sameType) {
    //Add dom
    newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT',
    }
}

For the dom node to be deleted, we don't need to create a new fiber. We just need to add an effectTag tag to the original fiber, but when we submit the fiber tree to the dom, it is from the working root fiber. The root fiber has no old fiber, so we need an array to store these dom nodes to be deleted, Therefore, you also need to define an array. The code is as follows:

if(oldFiber && !sameType) {
    //Delete dom
    oldFiber.effectTag = 'DELETION';
    deletions.push(oldFiber);
}
let deletions = null;

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot
    };
    deletions = [];
    nextUnitOfWork = wipRoot;
}

Then, when we submit the changed fiber to dom, we also need to use the fiber in this array, so we also need to optimize the commitRoot() method, and the code is as follows:

function commitRoot() {
    deletions.forEach(commitWork);
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}

Next, let's deal with our new effectTag tag in the commitWork() method. If the function of the effectTag tag is to add dom, our operation is the same as before. Add this dom node to the parent fiber. The code is as follows:

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    const domParent = fiber.parent.dom;

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

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

If it is marked to delete the dom, we will delete the dom from its parent fiber. The code is as follows:

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    const domParent = fiber.parent.dom;

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }else if(fiber.effectTag === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

If it is marked to update the dom, we need to update the existing dom node with the current element attribute, so we directly call a function to update the node here. This function is defined later, and the code is as follows:

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    const domParent = fiber.parent.dom;

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }else if(fiber.effectTag === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

Next, define the updatedom () method. To implement this method, we are actually comparing the old and new fiber s, and then delete useless attributes or update and set changed attributes. Therefore, before defining the updateDom() method, we need to use several additional methods to assist us in judgment, and then implement attribute deletion and update in the updateDom() method, as follows:

const isProperty = key => key != 'children';
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

function updateDom(dom, prevProps, nextProps) {
    //Delete old attribute
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //Set new properties or change properties
    Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
        dom[name] = nextProps[name];
    });
}

When processing attributes in the above code, we actually missed the events mounted on the node, so we need to continue to optimize the previous auxiliary judgment methods. We need to do special processing for attributes with prefix "on". The code is as follows:

const isEvent = key => key.startsWith('on');
const isProperty = key => key != 'children' && !isEvent(key);
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

Furthermore, we need to optimize the updateDom() method. The final code is as follows:

function updateDom(dom, prevProps, nextProps) {
    //Delete or change event listening
    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]);
    });

    //Delete old attribute
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //Set new properties or change properties
    Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
        dom[name] = nextProps[name];
    });

    //Add new event listener
    Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
    })
}

So far, we have completed the introduction of the reconciliation process. In fact, reconciliation is to update and delete dom nodes. If it corresponds to our code, it is actually to operate the old and new fiber s. When we save the code and view it at the front end, we can see the original output, and there is no error in the code. Let's change the component written by JSX and add a href attribute to it. We can see that it has been updated accordingly on the front page, and the hyperlink works normally, as follows:

/** @jsx XbcbLib.createElement */
const element = (
    <div id='xbcb'>
        <a href="http://www.xbcb. Top "> x Beichen North</a>
        <br />
    </div>
);

So far, all indexes The code of JS file is as follows. You can refer to and compare it:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map( value => {
                //typeof value == "object" ? value : createTextElement(value)
                if(typeof value == 'object') {
                    return value;
                }else {
                    return createTextElement(value)
                }
            })
        }
    }
}
function createTextElement(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: []
        }
    }
}

function createDom(fiber) {
    const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(fiber.type);

    const isProperty = key => key != 'children';
    Object.keys(fiber.props).filter(isProperty).forEach(name => {
        dom[name] = fiber.props[name];
    });

    return dom;

    // element.props.children.forEach(child => {
    //     render(child, dom);
    // });

    // container.appendChild(dom);
}

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = null;

const isEvent = key => key.startsWith('on');
const isProperty = key => key != 'children' && !isEvent(key);
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

function updateDom(dom, prevProps, nextProps) {
    //Delete or change event listening
    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]);
    });

    //Delete old attribute
    Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
        dom[name] = '';
    });

    //Set new properties or change properties
    Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
        dom[name] = nextProps[name];
    });

    //Add new event listener
    Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
    })
}

function commitRoot() {
    deletions.forEach(commitWork);
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    const domParent = fiber.parent.dom;

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }else if(fiber.effectTag === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot
    };
    deletions = [];
    nextUnitOfWork = wipRoot;
}

function workLoop(deadline) {
    let shouldYield = false;
    while(nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }

    if(!nextUnitOfWork && wipRoot) {
        commitRoot();
    }

    requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
    if(!fiber.dom) {
        fiber.dom = createDom(fiber);
    }

    // if(fiber.parent) {
    //     fiber.parent.dom.appendChild(fiber.dom);
    // }

    const elements = fiber.props.children;
    reconcileChildren(fiber, elements);

    if(fiber.child) {
        return fiber.child;
    }
    let nextFiber = fiber;
    while(nextFiber) {
        if(nextFiber.sibling) {
            return nextFiber.sibling;
        }
        nextFiber = nextFiber.parent;
    }
}

function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;

    while(index < elements.length || oldFiber != null) {
        const element = elements[index];
        let newFiber = null;

        const sameType = oldFiber && element && element.type === oldFiber.type;
        if(sameType) {
            //Update dom
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: 'UPDATE',
            }
        }
        if(element && !sameType) {
            //Add dom
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: 'PLACEMENT',
            }
        }
        if(oldFiber && !sameType) {
            //Delete dom
            oldFiber.effectTag = 'DELETION';
            deletions.push(oldFiber);
        }

        if(oldFiber) {
            oldFiber = oldFiber.sibling;
        }

        if(index == 0) {
            wipFiber.child = newFiber;
        }else {
            prevSibling.sibling = newFiber;
        }

        prevSibling = newFiber;
        index++;
    }
}

const XbcbLib = {
    createElement,
    render
};

/** @jsx XbcbLib.createElement */
const element = (
    <div id='xbcb'>
        <a href="http://www.xbcb. Top "> x Beichen North</a>
        <br />
    </div>
);

const container = document.getElementById('root');
XbcbLib.render(element, container);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Function component

After introducing the above parts, we will introduce the function components in this part. Because the JSX syntax components we have added are normal HTML tags, not custom components, we will continue to optimize our project to support function components.

Let's rewrite the original element component code to turn it into a function component, as follows:

/** @jsx XbcbLib.createElement */
function App(props) {
    return <h1>Hi, {props.name}</h1>;
}

const element = <App name="X Beichenbei" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

When we save the code directly at this time, the front-end page will report an error, because the current code does not support function component rendering. However, we know that if the JSX of this function component is converted to JS at this time, it should make the following changes:

/** @jsx XbcbLib.createElement */
function App(props) {
    return XbcbLib.createElement(
        'h1',
        null,
        'Hi',
        props.name
    )
}

const element = XbcbLib.createElement(App, {
    name: 'X Beichenbei',
});

Before we start, we need to know two things:

  • The fiber of the function component has no DOM node
  • The children attribute does not come directly from props, but from function calls

Therefore, we need to optimize the previous performnunitofwork () method, judge the type of fiber in it, and then decide to use different update methods to update and reconcile the fiber. If it is a function component, we use the update method of the function component. If it is not a function component, we use the original update method. The code is as follows:

function performUnitOfWork(fiber) {

    const isFunctionComponent = fiber.type instanceof Function;
    if(isFunctionComponent) {
        updateFunctionComponent(fiber);
    }else {
        updateHostComponent(fiber);
    }

    if(fiber.child) {
        return fiber.child;
    }
    let nextFiber = fiber;
    while(nextFiber) {
        if(nextFiber.sibling) {
            return nextFiber.sibling;
        }
        nextFiber = nextFiber.parent;
    }
}

function updateFunctionComponent(fiber) {

}

function updateHostComponent(fiber) {
    if(!fiber.dom) {
        fiber.dom = createDom(fiber);
    }

    const elements = fiber.props.children;
    reconcileChildren(fiber, elements);
}

In the update method of function components, we mainly get the children attribute. For example, in our sample code, its fiber Type is an App function, so we will return a dom element of h1 after calling it. If we get the children attribute, the next process is reconciliation. The reconciliation process is no different from our previous code. The code is as follows:

function updateFunctionComponent(fiber) {
    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children);
}

Because there is a fiber tree without dom nodes, we need to change the commitWork() method. Here we mainly change two parts. The first part is that we first need to find the parent node of the dom node. We need to look up along the fiber tree until we find the fiber with the dom node. Therefore, the code of the first part to be modified is as follows:

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    let domParentFiber = fiber.parent;
    while(!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent;
    }
    const domParent = domParentFiber.dom;

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }else if(fiber.effectTag === 'DELETION') {
        domParent.removeChild(fiber.dom);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

The second part is the node deletion part. We need to find the child node with dom node. The code is as follows:

function commitWork(fiber) {
    if(!fiber) {
        return;
    }
    
    let domParentFiber = fiber.parent;
    while(!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent;
    }
    const domParent = domParentFiber.dom;

    if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        domParent.appendChild(fiber.dom);
    }else if(fiber.effectTag === 'DELETION') {
        //domParent.removeChild(fiber.dom);
        commitDeletion(fiber, domParent);
    }else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
    if(fiber.dom) {
        domParent.removeChild(fiber.dom);
    }else {
        commitDeletion(fiber.child, domParent);
    }
}

So far, we have completed the support of function components. We define a component and then render it to the page, as follows:

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
    return <h1>Hi, {props.name}</h1>;
}

const element = <AppFunction name="X Beichenbei" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

Hooks

At present, our own react supports function components, but it still lacks state support, so let's see how to add state support. Here we use hooks to maintain the state in the function component. So let's rewrite the example code first, using the most classic counter example. The number of clicks will increase by 1. The code is as follows:

const XbcbLib = {
    createElement,
    render,
    useState,
};

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
    const [state, setState] = XbcbLib.useState(1);
    return (
        <h1 onClick={() => setState(c => c + 1)}>
            H1, {props.name}. The number of times you clicked is{state}. 
        </h1>
    )
}

const element = <AppFunction name="X Beichenbei" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

Then define the useState() method. Before defining this method, we also need to define some global variables for subsequent use in this method. The initialization of each variable is completed in the function component update method, as follows:

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
    wipFiber = fiber;
    hookIndex = 0;
    wipFiber.hooks = [];
    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children);
}

function useState(initial) {

}

In the above code, we set work as the fiber in progress and add an array of hooks to the fiber to support multiple calls to useState() in the same component. At the same time, we track the current hook index.

When the function component calls useState(), we check whether it has an old hook. Use the hook index to check the alternate attribute of fiber. If there is an old hook, we will copy the state from the old hook to the new hook, otherwise we will initialize the state. Then add a new hook to the fiber, and increase the hook index by 1, and then return to state. The code is as follows:

function useState(initial) {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
    const hook = {
        state: oldHook ? oldHook.state : initial,
    }

    wipFiber.hooks.push(hook);
    hookIndex++;
    return [hook.state];
}

At this time, we can see the expected effect on the front page after saving the code, but there is no response when we click. This is because useState() still needs to return a function to update state, so we need to define a setState() function in this method to receive an operation. We put this operation in a queue, Then, perform operations similar to those in the rendering process, and set the new unit of work in progress as the next unit of work, so that the new rendering stage can be cycled. The code is as follows:

function useState(initial) {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: [],
    }

    const setState = action => {
        hook.queue.push(action);
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
        };
        nextUnitOfWork = wipRoot;
        deletions = [];
    }

    wipFiber.hooks.push(hook);
    hookIndex++;
    return [hook.state, setState];
}

However, at present, we cannot run the action operations in the above code. We run these operations the next time we render the component. First, we get all the actions from the old hook queue, and then apply them to the state in the new hook one by one. Therefore, we will return the state after it is updated. The code is as follows:

function useState(initial) {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: [],
    }

    const actions = oldHook ? oldHook.queue : [];
    actions.forEach(action => {
        hook.state = action(hook.state);
    });

    const setState = action => {
        hook.queue.push(action);
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
        };
        nextUnitOfWork = wipRoot;
        deletions = [];
    }

    wipFiber.hooks.push(hook);
    hookIndex++;
    return [hook.state, setState];
}

So far, we have completed our react, and the click effect is as follows:

ending

This article is only to help us understand the workflow of react and pave the way for us to read the source code of react later. Therefore, variables and methods with the same name as those in react are used in our code. However, many react functions and optimizations are not included in our code. For example, we can look at some operations in react and how it does them:

  • In XbcbLib, we traverse the entire tree during the rendering phase. Instead, React follows some hints and heuristics to skip the entire subtree without any changes.
  • We are also traversing the entire tree in the submission phase. React only keeps the influential fibers and only accesses the linked list of those fibers.
  • Every time we create a new work in progress tree, we will create a new object for each fiber. React recycled the fiber in the previous tree.
  • When XbcbLib receives a new update in the rendering phase, it discards the work tree in progress and starts again from the root. React marks each update with an expiration timestamp and uses it to determine which update has a higher priority.
  • There are many similar

You can also add the following functions:

  • Use objects as style properties
  • Flatten subarray
  • useEffect hook
  • Key reconciliation