4

I. Introduction

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

The learning idea of this article comes from b station-React source code, you are on the first layer .

The simulated version is React 16.8.

The following functions will be implemented:

  1. createElement (virtual DOM) ;
  2. render
  3. can interrupt rendering ;
  4. Fibers
  5. Render and Commit Phases
  6. coordination (Diff algorithm) ;
  7. function component ;
  8. hooks

Let’s have dinner, please continue reading.

2. Preparation

1. React Demo

Let's take a look at a simple React Demo, the code is as follows:

const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);
For the complete source code of this example, see: reactDemo

Open reactDemo.html in the browser, the display is as follows:

image.png

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

1.1 element

const element = <div>123</div> actually JSX syntax.

React official website explains JSX as follows:

JSX is a JavaScript syntax extension. It is similar to a template language, but it has all the capabilities of JavaScript. JSX will eventually be compiled by babel into the React.createElement() function call.

Compile const element = <div>123</div> online through babel.

image.png

It can be seen that const element = <div>123</div> after being compiled is as follows:

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

Let's take a look at what kind of object actually generated by React.createElement above.

Try printing in the 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 is as follows:

image.png

Simplify the element:

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

To summarize briefly, React.createElement actually generates an element object, which has the following properties:

1.2 render

ReactDOM.render() adds the element to the DOM node whose id is container. Below we will simply hand-write a method instead of ReactDOM.render() .

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

    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 the text node text to the node node;

    node.appendChild(text)
  5. Add the node node to the container node

    container.appendChild(node)
For the complete source code of this example, see: reactDemo2

Run the source code, the results are as follows, and the results of the introduction of React are consistent:

image.png

Three, start

By simulating React, the React.createElement and ReactDOM.render methods are simply replaced above, and then the various functions of React will be realized.

1. createElement (virtual DOM)

I learned above that the role of createElement is to create an element object, the structure is as follows:

// 虚拟 DOM 结构
const element = {
    type: 'div', // 标签名
    props: { // 节点属性,包含 children
        title: 'foo', // title 属性
        children: 'hello' // 子节点,注:实际上这里应该是数组结构,帮助我们存储更多子节点
    }
}

According to the structure of element, the createElement function is designed, the code is as follows:

/**
 * 创建虚拟 DOM 结构
 * @param {type} 标签名
 * @param {props} 属性对象
 * @param {children} 子节点
 * @return {element} 虚拟 DOM
 */
function createElement (type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => 
                typeof child === 'object'
                ? child
                : createTextElement(child)
            )
        }
    }
}

It is considered here that when children are non-objects, a textElement element should be created. The code is as follows:

/**
 * 创建文本节点
 * @param {text} 文本值
 * @return {element} 虚拟 DOM
 */
function createTextElement (text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    }
}

Try it next, the code is as follows:

const myReact = {
    createElement
}
const element = myReact.createElement(
  "div",
  { id: "foo" },
  myReact.createElement("a", null, "bar"),
  myReact.createElement("b")
)
console.log(element)
For the complete source code of this example, see: reactDemo3

The element object obtained is as follows:

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, in the process of developing using react, we do not 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 is as follows:

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

In myReact, you can use JSX syntax by adding comments to tell babel to translate the function we specify. The code is as follows:

/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)
For the complete source code of this example, see: reactDemo4

2. render

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

The implementation will be divided into the following steps:

  1. Create a dom node of type element.type and add it to the container;
/**
 * 将虚拟 DOM 添加至真实 DOM
 * @param {element} 虚拟 DOM
 * @param {container} 真实 DOM
 */
function render (element, container) {
    const dom = document.createElement(element.type)
    container.appendChild(dom)
}
  1. Add 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 props attribute of the element to the dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
})

Above we have realized the function of rendering JSX to the real DOM, let's try it next, 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'))
For the complete source code of this example, see: reactDemo5

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

image.png

3. Interruptible rendering (requestIdleCallback)

Let's take a look at the processing of child nodes in the render method written above. The code is as follows:

/**
 * 将虚拟 DOM 添加至真实 DOM
 * @param {element} 虚拟 DOM
 * @param {container} 真实 DOM
 */
function render (element, container) {
    // 省略
    // 遍历所有子节点,并进行渲染
    element.props.children.forEach(child =>
        render(child, dom)
    )
    // 省略
}

This recursive call is problematic. Once the rendering is started, all nodes and their child nodes will be rendered completely 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 performed.

It can be divided into the following steps to solve the above problems:

  1. It is allowed to interrupt the rendering work. If there is a higher priority work inserted, the browser rendering will be temporarily interrupted, and the browser rendering will be resumed after the work is completed;
  2. Decompose the rendering work into small units;

Use requestIdleCallback to solve the problem of allowing interruption of rendering work.

window.requestIdleCallback queues up functions that are called during the browser's idle time. This enables developers to perform background and low-priority work on the main event loop without affecting delayed key events such as animation and input response.

window.requestIdleCallback Detailed introduction can be found in the document: document

code show as below:

// 下一个工作单元
let nextUnitOfWork = null
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止时间
 */
function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)

// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

performUnitOfWork is used to execute the unit event and return to the next unit event. The specific implementation will be introduced below.

4. Fiber

The above introduced the use of requestIdleCallback to allow the browser to render the work unit in idle time to avoid the problem of page jams caused by too long rendering.

Note: In fact, the requestIdleCallback function is not stable, and it is not recommended to be used in a production environment. This example is only used to simulate the idea of React. React itself does not use requestIdleCallback to allow the browser to render the unit of work in its free time.

On the other hand, in order to allow rendering work to be separated into small units, React designed fiber.

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

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

The following will introduce the fiber through a simple example.

Suppose you need to render such an element tree:

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

The generated fiber tree is shown in the figure:

Orange represents a child node, yellow represents a parent node, and blue represents a sibling node.

image.png

Each fiber has a link to its first child node, next sibling node, and its parent node. This data structure allows us to find the next unit of work more conveniently.

The arrow in the figure above also indicates the rendering process of the fiber, which is described in detail as follows:

  1. Starting from root, find the first child node div;
  2. Find the first child node h1 of div;
  3. Find the first child node p of h1;
  4. Find the first child node of p, if no child nodes, find the next sibling node , and find the sibling node a of p;
  5. Find the first child node of a. no child nodes and no sibling nodes, then find the next sibling node its parent node, and find the sibling node h2 of the parent node of a;
  6. Look for the first child node of h2, and find it, look for a sibling node, but not find it, look for the sibling node of the parent node div, but also can’t find it, continue to look for the sibling node of the parent node of div, and find the root;
  7. The root node has been found in step 6, and the rendering has been completed.

The rendering process will be implemented in code below.

  1. Extract the part that creates the DOM node in the render into the creactDOM function;
/**
 * createDom 创建 DOM 节点
 * @param {fiber} fiber 节点
 * @return {dom} dom 节点
 */
function createDom (fiber) {
    // 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)

    // isProperty 表示不是 children 的属性
    const isProperty = key => key !== "children"
    
    // 遍历 props,为 dom 添加属性
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = fiber.props[name]
        })
        
    // 返回 dom
    return dom
}
  1. Set the first unit of work as the fiber root node in render;

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

// 下一个工作单元
let nextUnitOfWork = null
/**
 * 将 fiber 添加至真实 DOM
 * @param {element} fiber
 * @param {container} 真实 DOM
 */
function render (element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        }
    }
}
  1. Render the fiber when the browser is idle through requestIdleCallback;
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止时间
 */
function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
  1. The function performUnitOfWork that renders the fiber;
/**
 * performUnitOfWork 处理工作单元
 * @param {fiber} fiber
 * @return {nextUnitOfWork} 下一个工作单元
 */
function performUnitOfWork(fiber) {
  // TODO 添加 dom 节点
  // TODO 新建 filber
  // TODO 返回下一个工作单元(fiber)
}

4.1 Add dom node

function performUnitOfWork(fiber) {
    // 如果 fiber 没有 dom 节点,为它创建一个 dom 节点
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    // 如果 fiber 有父节点,将 fiber.dom 添加至父节点
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
    }
}

4.2 New Filber

function performUnitOfWork(fiber) {
    // ~~省略~~
    // 子节点
    const elements = fiber.props.children
    // 索引
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null
    // 遍历子节点
    while (index < elements.length) {
        const element = elements[index]

        // 创建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // 将第一个子节点设置为 fiber 的子节点
        if (index === 0) {
            fiber.child = newFiber
        } else if (element) {
        // 第一个之外的子节点设置为该节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
}

4.3 Return to the next unit of work (fiber)


function performUnitOfWork(fiber) {
    // ~~省略~~
    // 如果有子节点,返回子节点
    if (fiber.child) {
        return fiber.child
    }
    let nextFiber = fiber
    while (nextFiber) {
        // 如果有兄弟节点,返回兄弟节点
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }

        // 否则继续走 while 循环,直到找到 root。
        nextFiber = nextFiber.parent
    }
}

Above we have implemented the function of rendering the fiber to the page, and the rendering process is interruptible.

Try it now, the code is as follows:

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

myReact.render(element, document.getElementById('container'))
For the complete source code of this example, see: reactDemo7

Output dom as expected, as shown in the figure:

image.png

5. Rendering submission stage

Since the rendering process is made interruptible by us, when interrupted, we definitely don't want the browser to show the user a half-rendered UI.

The optimized processing for the rendering submission stage is as follows:

  1. Delete the logic of adding child nodes to the parent node in performUnitOfWork;
function performUnitOfWork(fiber) {
    // 把这段删了
    if (fiber.parent) {
       fiber.parent.dom.appendChild(fiber.dom)
    }
}
  1. Add a new root node variable to store the fiber root node;
// 根节点
let wipRoot = null
function render (element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        }
    }
    // 下一个工作单元是根节点
    nextUnitOfWork = wipRoot
}
  1. When all fibers are finished, nextUnitOfWork is undefined, and then the real DOM is rendered;
function workLoop (deadline) {
    // 省略
    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }
    // 省略
}
  1. Added the commitRoot function to perform the operation of rendering the real DOM and recursively render the fiber tree into the real DOM;
// 全部工作单元完成后,将 fiber tree 渲染为真实 DOM;
function commitRoot () {
    commitWork(wipRoot.child)
    // 需要设置为 null,否则 workLoop 在浏览器空闲时不断的执行。
    wipRoot = null
}
/**
 * performUnitOfWork 处理工作单元
 * @param {fiber} fiber
 */
function commitWork (fiber) {
    if (!fiber) return
    const domParent = fiber.parent.dom
    domParent.appendChild(fiber.dom)
    // 渲染子节点
    commitWork(fiber.child)
    // 渲染兄弟节点
    commitWork(fiber.sibling)
}
For the complete source code of this example, see: reactDemo8

The results of the source code operation are as follows:

image.png

6. Coordination (diff algorithm)

When the element is updated, you need to compare the fiber tree before the update with the fiber tree after the update. After the comparison result is obtained, only the dom node corresponding to the changed fiber is updated.

Through coordination, reduce the number of operations on the real DOM.

1. currentRoot

Added the currentRoot variable to save the fiber tree before the root node update, and add an alternate attribute to the fiber to save the fiber tree before the fiber update;

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

2. performUnitOfWork

Extract the logic of creating a new fiber in performUnitOfWork to the reconcileChildren function;

/**
 * 协调子节点
 * @param {fiber} fiber
 * @param {elements} fiber 的 子节点
 */
function reconcileChildren (fiber, elements) {
    // 用于统计子节点的索引值
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null

    // 遍历子节点
    while (index < elements.length) {
        const element = elements[index]

        // 新建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // fiber的第一个子节点是它的子节点
        if (index === 0) {
            fiber.child = newFiber
        } else if (element) {
        // fiber 的其他子节点,是它第一个子节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        // 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了
        prevSibling = newFiber
        
        // 索引值 + 1
        index++
    }
}

3. reconcileChildren

Compare the old and new fibers in reconcileChildren;

3.1 When the new and old fiber types are the same

Keep the dom, only update the props, and set the effectTag to UPDATE;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    // oldFiber 可以在 wipFiber.alternate 中找到
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child

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

        // fiber 类型是否相同
        const sameType =
            oldFiber &&
            element &&
            element.type == oldFiber.type

        // 如果类型相同,仅更新 props
        if (sameType) {
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }
        // ~~省略~~
    }
    // ~~省略~~
}
3.2 When the old and new fiber types are different and there are new elements

Create a new dom node and set effectTag to PLACEMENT;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (element && !sameType) {
        newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: "PLACEMENT",
        }
    }
    // ~~省略~~
}
3.3 When the new and old fiber types are different and there is an old fiber

Delete the old fiber and set the effectTag to DELETION;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (oldFiber && !sameType) {
        oldFiber.effectTag = "DELETION"
        deletions.push(oldFiber)
    }
    // ~~省略~~
}

4. deletions

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

let deletions = null
function render (element, container) {
    // 省略
    // render 时,初始化 deletions 数组
    deletions = []
}

// 渲染 DOM 时,遍历 deletions 删除旧 fiber
function commitRoot () {
    deletions.forEach(commitWork)
}

5. commitWork

Judge the effectTag of the fiber in commitWork and handle them separately.

5.1 PLACEMENT

When the effectTag of the 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 the fiber is DELETION, it means that the fiber is deleted, and the node of the parent node is deleted.

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

When the effectTag of the fiber is UPDATE, it means that the fiber is updated and the props attribute is updated.

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

The updateDom function updates the props attribute according to different update types.

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) {
    // 删除旧属性
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })

    // 更新新属性
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })
}

In addition, update and delete event attributes are added to updateDom to facilitate tracking of fiber event updates.

function updateDom(dom, prevProps, nextProps) {
    // ~~省略~~
    const isEvent = key => key.startsWith("on")
    //删除旧的或者有变化的事件
    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]
        )
    })
    // ~~省略~~
}

Replace the logic of 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 that contains input form items, try to update the element, the code is as follows:

/** @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")
For the complete source code of this example, see: reactDemo9

The output result is as follows:

12.gif

7. Functional components

Let's first look at a simple example of a functional component:

myReact does not support functional components. The code below will report an error. This is only used to compare the normal usage of functional components.
/** @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)

Compared with the html tag component, the functional component has the following two differences:

  • The fiber of the function component does not have a dom node;
  • The children of the function component need to be obtained after running the function;

Implement the functional component through the following steps:

  1. Modify performUnitOfWork to execute the fiber unit of work according to the fiber type;
function performUnitOfWork(fiber) {
    // 是否是函数类型组件
    const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
    // 如果是函数组件,执行 updateFunctionComponent 函数
    if (isFunctionComponent) {
        updateFunctionComponent(fiber)
    } else {
    // 如果不是函数组件,执行 updateHostComponent 函数
        updateHostComponent(fiber)
    }
    // 省略
}
  1. Define the updateHostComponent function to execute non-functional components;

Non-functional components can directly pass fiber.props.children as a parameter.

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

The function component needs to be run to obtain fiber.children.

function updateFunctionComponent(fiber) {
    // fiber.type 就是函数组件本身,fiber.props 就是函数组件的参数
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}
  1. Modify the commitWork function to be compatible with fibers without dom nodes;

4.1 Modify the acquisition logic of domParent, and continuously search upwards through the while loop until the parent fiber with the dom node is found;

function commitWork (fiber) {
    // 省略
    let domParentFiber = fiber.parent
    // 如果 fiber.parent 没有 dom 节点,则继续找 fiber.parent.parent.dom,直到有 dom 节点。
    while (!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
    }
    const domParent = domParentFiber.dom
    // 省略
}

4.2 Modify the logic of deleting a node. When deleting a node, you need to keep looking down until you find a child fiber with a dom node;

function commitWork (fiber) {
    // 省略
    // 如果 fiber 的更新类型是删除,执行 commitDeletion
     else if (fiber.effectTag === "DELETION") {
        commitDeletion(fiber.dom, domParent)
    }
    // 省略
}

// 删除节点
function commitDeletion (fiber, domParent) {
    // 如果该 fiber 有 dom 节点,直接删除
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
    // 如果该 fiber 没有 dom 节点,则继续找它的子节点进行删除
        commitDeletion(fiber.child, domParent)
    }
}

Let's try 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)
For the complete source code of this example, see: reactDemo10

The results of the operation are shown in the figure:

image.png

8. hooks

Let's continue to add the function of managing state to myReact. The expectation is that the functional component has its own state, and can get and update the state.

A functional component with counting 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 obtain and update the state.

To reiterate here, rendering function component is to execute the function component . Therefore, if the Counter above wants to update the count, it will execute the Counter function once for each update.

Through the following steps:

  1. Added global variable wipFiber;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
    wipFiber = fiber
    // 当前工作单元 fiber 的 hook
    wipFiber.hook = []
    // 省略
}
  1. Added useState function;
// initial 表示初始参数,在本例中,initial=1
function useState (initial) {
    // 是否有旧钩子,旧钩子存储了上一次更新的 hook
    const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hook

    // 初始化钩子,钩子的状态是旧钩子的状态或者初始状态
    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.hook = hook
    
    // 返回钩子的状态和设置钩子的函数
    return [hook.state, setState]
}

Let's run the counting component, the code is as follows:

function Counter() {
    const [state, setState] = myReact.useState(1)
    return (
        <h1 onClick={() => setState(c => c + 1)}>
        Count: {state}
        </h1>
    )
}
const element = <Counter />
For the complete source code of this example, see: reactDemo11

The results of the operation are shown in the figure:
123.gif

This chapter simply implements the hooks function of myReact.

After the flower is over, there are many implementations of react that are worthy of our study and research. I hope that in the next issue, I will write more functions of react with you.

to sum up

This article refers to pomb.us for learning, and realizes custom React including virtual DOM, Fiber, Diff algorithm, functional components, hooks and other functions.

In the implementation process, the editor has a general grasp of the basic terminology and implementation ideas of React. pomb.us is a learning material very suitable for beginners. You can directly learn through pomb.us . It is also recommended to follow this article. Step by step to realize the common functions of React.

The source code of this article: github source code .

It is recommended to follow step by step and perform practical exercises.

Hope it can be helpful to you, thanks for reading~

Don’t forget to give me a like and encourage me, refill ❤️

Reference

Or follow the AOTULabs official account (AOTULabs) and push articles from time to time.


凹凸实验室
2.3k 声望5.5k 粉丝

凹凸实验室(Aotu.io,英文简称O2) 始建于2015年10月,是一个年轻基情的技术团队。