Writing in the front

  • I started this blog when I was browsing the community and found pomb.us/build-your-… In this article, the blogger implements a simple version of React based on the Fiber architecture after React16, which is very helpful in understanding how React works. But it suffers from the fact that it is only available in English and tends to be theoretical.

  • In line with the concept of self-improvement, contribution to the community. Here I will record my learning process and try my best to translate the key parts (combined with my own understanding). I hope it helps.

Zero, preparation

  1. Create the project (name it yourself) and download the package

    $ mkdir xxx
    $ cd xxx
    $ yarn init -y / npm init -y
    $ yarn add react react-dom
    Copy the code
  2. Create the following directory structure

    - src/
     - myReact/
      - index.js
     - index.html
     - main.jsx
    Copy the code
  3. Initialize the file contents

    //index.html<! DOCTYPE html><html lang="en">
        <head>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>React App</title>
        </head>
        <body>
            <div id="root"></div>
            <script src="main.jsx"></script>
        </body>
    </html>
    
    
    // main.jsx
    import React from "react";
    import ReactDom from "react-dom";
    const App = () = > {
        return <div title="oliver">Hello</div>;
    };
    ReactDom.render(<App />.document.getElementById("root"));
    
    // myReact/index.js
    export default {}
    Copy the code
  4. Install Parcel for packaging and hot updates

    $ yarn add parcel-bundler
    Copy the code

The createElement function

Babel

// main.jsx
const element = (
  <div id="foo">
    <a>Hello</a>
    <span />
  </div>
)
Copy the code

After Babel translation effect (using the plugin – transform – react – JSX plug-in, www.babeljs.cn/docs/babel-.) :

const element = React.createElement(
  "div".//type
  { id: "foo" },	//config
  React.createElement("a".null."bar"),	/ /... children
  React.createElement("span"))Copy the code
  • The Babel ofplugin-transform-react-jsxWhat you do is simple: useReact.createElementFunction to handle JSX syntax from. JSX files.
  • That’s why in.jsx files you have toimport React from "react"Otherwise the plugin will not find the React object!

Configure the Babel

Tips: I also intended to use the plugin-transform-React-jsx plug-in, but encountered problems in debugging.

Hello World

to React. CreateElement (‘h1’, null, ‘Hello world’) simple conversion (see zh-hans.reactjs.org/blog/2020/0…). Therefore, transform-jsx with similar functions is chosen as the next best choice

$ touch .babelrc
$ yarn add babel@transform-jsx
Copy the code
// .babelrc
{
    "presets": ["es2015"]."plugins": [["transform-jsx",
      {
        "function": "React.createElement"."useVariables": true}}]]Copy the code
$ parcel src/index.html
Copy the code

At this point, you can see the word Hello in the page, indicating that we successfully configured!

createElement

The transform-JsX plug-in wraps the parameters in an object, passing createElement.

// myReact/index.js
export function createElement(args) {
  const { elementName, attributes, children } = args;
  return {
    type:elementName,
    props: {
      ...attributes,
      children
    }
  };
}
Copy the code

Consider that children may also contain basic types such as string, number. To simplify operations, we wrap such children uniformly with TEXT_ELEMENT.

// myReact/index.js
export function createElement(type, config, ... children) {
  return {
    type,
    props: {
      ...attributes,
      children: children.map((child) = >
                typeof child === "object" ? child : createTextElement(child)
            ),
    }
  };
}
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT".props: {
      nodeValue: text,
      children: [],},}}export default { createElement }
Copy the code

React doesn’t handle basic type nodes like this, but let’s do it here: it simplifies our code. After all, this is an article about features, not details.

See the effect

Let’s start by naming our library.

// .babelrc
{
    "presets": ["es2015"]."plugins": [["transform-jsx",
      {
        "function": "OllyReact.createElement"."useVariables": true}}]]Copy the code

Use your own name when you introduce it!

// main.jsx
import OllyReact from "./myReact/index";
import ReactDom from "react-dom"
const element = (
    <div style="background: salmon">
        <h1>Hello World</h1>
        <h2 style="text-align:right">- Oliver</h2>
    </div>
);
ReactDom.render(element, document.getElementById("root"));
Copy the code

Hello appears on the page, which shows that our React. CreateElement basically implements the React function.

2. Render function

Next, write the render function.

For now, we’ll just focus on adding content to the DOM. Modify and delete functions will be added later.

// React/index.js
export function render(element, container) {}
export default {
  / /... omit
  render
};
Copy the code

Details of the implementation

Note:

Each step of this section can refer to the idea, and the detailed logical sequence will be summarized at the bottom.

  • Start by creating a new DOM node with the corresponding element type and adding the DOM node to the stock Container

    const dom = document.createElement(element.type)
    container.appendChild(dom)
    Copy the code
  • The same operation is then recursively performed for each child JSX element

      element.props.children.forEach(child= >
        render(child, dom)
      )
    Copy the code
  • Consider that the TEXT node requires special handling

    const dom =
        element.type == "TEXT_ELEMENT"
          ? document.createTextNode("")
          : document.createElement(element.type)
    Copy the code
  • Finally, assign the props of the element to the real DOM node

    Object.keys(element.props)
            .filter(key= >key ! = ="children")	// The children attribute should be removed.
            .forEach(name= > {
              dom[name] = element.props[name];
            });
    Copy the code

Summary:

export function render(element, container) {
  const dom = element.type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type);
  Object.keys(element.props)
        .filter(key= >key ! = ="children")
        .forEach(name= > {
          dom[name] = element.props[name];
        });
  element.props.children.forEach(child= >
    render(child, dom)
  );
  container.appendChild(dom);
}
Copy the code

See the effect

// main.jsx
import OllyReact from "./myReact/index";
const element = (
    <div style="background: salmon">
        <h1>Hello World</h1>
        <h2 style="text-align:right">- Oliver</h2>
    </div>
);
OllyReact.render(element, document.getElementById("root"));
Copy the code

Now we can see that our render function works as well!

summary

That’s it! Now we have a library that can render JSX to the DOM (although it only supports native DOM tags and does not support updating QAQ).

Concurrent mode Indicates the concurrent mode

In fact, the recursive invocation above is problematic.

  1. This way, once we start rendering, we will not stop until we have rendered the complete element tree. If the element tree is large, it may block the main line for too long.
  2. Even if the browser needs to perform high-priority work such as processing user input, it must wait for rendering to complete.

Thus React16’s Concurrent mode implements an asynchronous interruptible way of working. It breaks the work down into small units and, after completing each unit, tells the browser to interrupt rendering if anything else needs to be done.

workLoop

let nextUnitOfWork = null

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

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // todo
}
Copy the code
  • We userequestIdleCallbackLet’s do a loop. It can berequestIdleCallbackAs an asynchronous task, the browser will run the callback when the main thread is idle, rather than telling us when to run it.
  • requestIdleCallbackWe are also given a deadline parameter. We can use it to check how much time the browser needs to control again.
  • To start using a loop, we need to set up our first unit of work and then write oneperformUnitOfWorkFunction. It is required not only to execute the current unit of work, but also to return the next unit of work.

Fourth, the Fiber

To organize the structure of the units of work, we need a Fiber tree.

The function of the Fiber

  1. Static data structures (virtual DOM)
  2. As schema: Connect parent, child, and sibling nodes
  3. As a unit of work

Fiber Tree Organizational form

  • Create one in RenderrootFiberNode, and make it the firstNextUnitOfWork (an Instance of Fiber)The incoming
  • performUnitOfWorkacceptnextUnitOfWorkAs parameters and do three things:
    1. Add the corresponding fiber node to the DOM
    2. Create a subfiber node of the fiber node
    3. Select the next unit of work

The purpose of this data structure is to make it easier to find the next unit of work:

  1. After the current work on Fiber is completed, iffiber.child! ==null,fiber.childThe node will be the next unit of work.
  2. The current Fiber has no child nodes, thenfiber.sibling! ==nullIn the case of,fiber.siblingThe node will be the next unit of work.
  3. Current Fiber nodefiber.child===null && fiber.sibiling===nullIn the case of,fiber.parentThe node’ssiblingThe node will be the next unit of work.
  4. Back to rootFiber proof finished render work.

Refactor the code

// Separate the logic that creates the DOM element from the render method
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
}

// Initialize the rootFiber root node in the Render node
export function render(element, container) {
    nextUnitOfWork = {  //rootFiber
    dom: container,
    props: {
      children: [element]
    },
  }
}

function workLoop() {... }function performUnitOfWork(){
    //todo
}
requestIdleCallback(workLoop)
Copy the code

Then, when the browser is ready, it will call our workLoop and we will start working on the root directory.

performUnitOfWork

Function 1
function performUnitOfWork() {
  //******** Function 1: Create dom ********
  if(! fiber.dom) {// Bind the dom to the fiber node
    fiber.dom = createDom(fiber);
  }
  if (fiber.parent) {   // If a parent node exists, mount it to the parent nodefiber.parent.dom.appendChild(fiber.dom); }}Copy the code
Function 2
function performUnitOfWork() {.../ / * * * * * * * * function 2: create fiber for children of JSX element node and link * * * * * * * *
  const elements = fiber.props.children;
  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) {  // The first subfiber is children
      fiber.child = newFiber;
    } else {  // The other subfibers are connected by sibling in turnprevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}Copy the code
Function 3
function performUnitOfWork() {...//******** Function 3: returns the next unit of work ********
  if (fiber.child) return fiber.child;  // If the child node exists, return the child node
  let nextFiber = fiber;
  while (nextFiber) {   // If the child node does not exist, look for the sibling node or the sibling node of the parent node
    if (nextFiber.sibling) {
      returnnextFiber.sibling; } nextFiber = nextFiber.parent; }}Copy the code

Render phase & Commit phase

Here we have another problem.

Because every time you work with a fiber, you create a DOM and insert a new node. And the render in the Fiber architecture is interruptible. This makes it possible for users to see an incomplete UI. This is not what we want.

So we need to remove the DOM insertion.

function performUnitOfWork(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }// if (fiber.parent) {
  // fiber.parent.dom.appendChild(fiber.dom)
  // }
  const elements = fiber.props.children
}
Copy the code

Instead, we trace the root node of the Fiber Tree, called wipRoot

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

After the workLoop completes (no nextUnitOfWork exists), commitRoot commits the entire Fiber tree to the renderer.

function workLoop() {...if(! nextUnitOfWork && wipRoot) { commitRoot() } ... }Copy the code

CommitWork is used to process each unit of work

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)
}
Copy the code

Sixth, Reconcilation

So far we’ve only implemented adding the DOM, so how do we update or remove it?

That’s what we’re going to do now: compare the Fiber tree we received in the Render function to the last Fiber tree we submitted.

currentRoot

So we need a pointer to the last Fiber tree, let’s call it currentRoot.

let currentRoot = null
function commitRoot() {
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
Copy the code

alternate

On each fiber node number, add an alternate property that points to the old fiber node.

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

reconcileChildren

The code of creating Fiber node is extracted from performUnitOfWork and extracted into reconcileChildren method.

In this method, we diff the new JSX element with the old Fiber node.

function reconcileChildren(fiber, 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) {  // The first subfiber is children
      fiber.child = newFiber;
    } else {  // The other subfibers are connected by sibling in turnprevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}Copy the code

The details of diff’s process follow, which will not be covered here.

Function component support

Goal:

import OllyReact from "./myReact/index";

const App = () = > {
  const element = (
    <div style="background: salmon">
      <h1>Hello World</h1>
      <h2 style="text-align:right">- Oliver</h2>
    </div>
  );
  return element;
};
OllyReact.render(<App/>.document.getElementById("root"));
Copy the code

Main differences between functional components and native components:

  1. FibernodesFiber.domnull
  2. childrenYou need to execute the function component to get it, instead of getting it directly from the props

Special handling of function components

function performUnitOfWork() {
      const isFunctionComponent =
      fiber.type instanceof Function
      if (isFunctionComponent) {
        updateFunctionComponent(fiber)
      } else {
        updateHostComponent(fiber)
      }
      ...
}
    
function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)];	// Get the JSX element by executing the function component
  reconcileChildren(fiber, children);
}
    
function updateHostComponent(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber); } reconcileChildren(fiber, fiber.props.children); }function commitWork() {...let domParentFiber = fiber.parent;  // Iterate up until you find the parent fiber with fiber.dom
    while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent; }const domParent = domParentFiber.dom
}
    
function commitDeletion(fiber, domParent) { // When deleting a node, we need to continue until we find a child node with a DOM node.
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else{ commitDeletion(fiber.child, domParent); }}Copy the code

Eight, Hooks

Classic counter

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

Add some auxiliary variables to the Hook

let wipFiber = null		// Current workInProgress Fiber node
let hookIndex = null	/ / hooks subscript

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []	// Maintain a separate array of hooks for each fiber node
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
Copy the code

Write useState

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  
  const hook = {
    state: oldHook ? oldHook.state : initial,	// If an old value exists, use the old value; otherwise, use the initial value.
    queue: []}const actions = oldHook ? oldHook.queue : []
  actions.forEach(action= > {	// Iterate through each action in the old hook.queue, executing it in sequence
    hook.state = action(hook.state)
  })
  
  const setState = action= > {
    hook.queue.push(action)
    wipRoot = {	// Switch to the fiber tree
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot	// Reset nextUnitOfWork to trigger the update.
    deletions = []
  }
  wipFiber.hooks.push(hook)	// Push the current useState call into hooks
  hookIndex++	// hooks array subscript +1, pointer backward
  return [hook.state, setState]
}
Copy the code

From this section, we can get some ideas about hooks.

  1. Why can’t hooks be in if?

    • In this case: because each hook is maintained in an array of hooks on the fiber node in the order they are called. If one of the hooks is in the if statement, it may disrupt the proper order of the array. This will lead to an error in the corresponding hook.

    • In React: use the next pointer to concatenate hooks, which again can’t tolerate a mess of order.

      type Hooks = { 
          memoizedState: any, // Point to the current render node Fiber
          baseState: any, // Initialize initialState, which has been newState after each dispatch
          baseUpdate: Update<any> | null.Update = Update = Update = Update = Update = Update = Update = Update = Update = Update
          queue: UpdateQueue<any> | null./ / UpdateQueue through
          next: Hook | null.// Link to the next hooks, concatenating each with next
      }
      Copy the code
  2. The capture Value features

    • Capture Value is nothing special. It’s just a closure.

    • Every time you trigger Rerender, you re-execute the function component. Then the lexical environment of the last executed function component should be reclaimed. However, the lexical environment will still exist for some time because of closures that are kept in hooks such as useEffect.