In this article, we wrote a simple React by hand to see what the Facebook team was doing with Fiber architecture, which they rebuilt over two years ago. Thus, we have an intuitive understanding of the basic principle of React. Ga: Let’s get started
Bronze — How do React, JSX, DOM Elements work?
React 16.8 is implemented in this article.
Let’s implement a simple page rendering to quickly understand the connection between JSX, React, and DOM elements.
import React from "react";
import ReactDOM from "react-dom";
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
Copy the code
A simple React application requires only three lines of code 👆.
- Create React elements
- Obtain the root node root
- Render the React element onto the page
1. How is JSX parsed – Babel
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
Copy the code
Create a react element with JSX. It is not a valid JS element. It is actually parsed by Babel as follows:
"use strict";
const element = /*#__PURE__*/ React.createElement(
"div",
{
id: "foo",},/*#__PURE__*/ React.createElement("a".null."bar"),
/*#__PURE__*/ React.createElement("b".null));Copy the code
As you can see, Babel will convert JSX to the react.createElement () method, where the createElement() method takes three arguments: element type, element attribute props, and children. We will implement this method later.
2. React virtual DOM object design
React maintains a virtual DOM tree in memory, updates the virtual DOM when data changes, and creates a new tree. Diff the old and new virtual DOM trees, finds the changed parts, and creates a Change(Patch). Finally, batch update these patches into DOM.
Let’s start with the basic virtual DOM structure:
const element = {
type: "div".props: {
id: "foo".children: [{type: "a".props: {
children: ["bar",}}, {type: "b".props: {
children: [],},},],},};Copy the code
You can see that the react.createElement () method simply returns a virtual DOM object. Now let’s implement the createElement() method,
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map((child) = >
CreateTextElement creates the text node type with createTextElement
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT".props: {
nodeValue: text,
children: [].}}; }Copy the code
You can see that the element object compiled by Babel is actually the data structure returned by a recursive call to react.createElement () – a nested virtual DOM structure.
Implement the render() method
With the virtual DOM structure in place, the next step is to generate real nodes from it and render them on the page, which is what the Render () method does. It is basically divided into the following four steps:
- Create different types of nodes
- Adding properties props
- Iterate over children, recursively calling Render
- Append the generated node to the root node
function render(element, container) {
// 1. Create different types of DOM nodes
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 2. Add props to the DOM node.
const isProperty = (key) = >key ! = ="children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) = > {
dom[name] = element.props[name];
});
// 3. Iterate over children, recursively calling render
element.props.children.forEach((child) = > render(child, dom));
// 4. Add the DOM node to the root node
container.appendChild(dom);
}
Copy the code
When using JSX syntax, Babel compiles the React. CreateElement method by default. This is why app.tsx import files do not explicitly use React. So how do you tell Babel to compile with its own createElement method? JSX supports the following comment to tell Babel to compile using the specified method:
const MyReact = {
createElement,
render,
};
/ * *@jsx MyReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
function createElement() {
/ /...
}
Copy the code
This allows us to quickly understand the connection between JSX, React, and DOM elements by implementing a simple page rendering. At this point, we have a simple React that renders JSX to the page.
Silver — Fiber architecture
But there are problems with the recursive invocation of the first part. Once we start rendering, we can’t stop until we’ve rendered the entire DOM tree. What’s the problem?
In the browser, the page is drawn frame by frame. In general, the screen refresh rate of the device is 1s 60 times, and each frame takes about 16ms to draw. The browser has a lot to do in this frame! If the task is particularly long at one stage, and the time is significantly longer than 16ms, then the rendering of the page will block, resulting in stalling, also known as frame drop!
React 16 uses this recursive call traversal mode. The execution stack gets deeper and deeper and cannot be interrupted. After an interrupt, it cannot be recovered. If the recursion takes 100ms, the browser will not be able to respond to the 100ms, and the longer the code executes, the more sluggish it becomes.
React introduced Fiber architecture to address the above pain points. How does the React Fiber architecture work?
- Work tasks are performed by breaking them into small work units -> Fiber
- React rendering can be interrupted, and control can be handed back to the browser to allow it to respond to user interactions in a timely manner -> asynchronously interruptible
window.requestIdleCallback()
The React rendering process can be interrupted, and control can be handed back to the browser so that it can respond to user interactions in a timely manner. When the browser is idle, it can resume rendering.
RequestIdleCallback (callback, {timeout: 1000}) allows the browser to execute our callback whenever it is’ free ‘.
Let’s take a quick look at a requestIdleCallback example
// Define a task queue
let taskQueue = [
() = > {
console.log("task1 start");
console.log("task1 end");
},
() = > {
console.log("task2 start");
console.log("task2 end");
},
() = > {
console.log("task3 start");
console.log("task3 end"); },];// Execute the unit of work. Fetch the first task in the queue each time and execute it
let performUnitOfWork = () = > {
taskQueue.shift()();
};
/** * Callback receives the default deadline argument, and the timeRamining attribute indicates how much time is left in the current frame */
let workloop = (deadline) = > {
console.log('Remaining time of this frame -->${deadline.timeRemaining()} ms`);
// The remaining time of this frame is greater than 0
while (deadline.timeRemaining() > 0 && taskQueue.length > 0) {
performUnitOfWork();
console.log('Time left:${deadline.timeRemaining()} ms`);
}
// Otherwise you should give up execution control and hand it back to the browser.
if (taskQueue.length > 0) {
// Request the next time slicerequestIdleCallback(workloop); }};// Register the task, which tells the browser that it can execute the registered task if there is free time in each frame
requestIdleCallback(workloop);
Copy the code
You can see that with 15ms left in the current frame, the browser has completed all three tasks in sequence, and the current frame has plenty of time. Add a sleep time of 20ms, that is, each task is longer than a frame of 16ms, that is, after each task is executed, the current frame is out of time, and the control needs to be handed over to the browser
// Each task is over 16ms
let taskQueue = [
() = > {
console.log("task1 start");
sleep(20);
console.log("task1 end");
},
() = > {
console.log("task2 start");
sleep(20);
console.log("task2 end");
},
() = > {
console.log("task3 start");
sleep(20);
console.log("task3 end"); },];let sleep = (delay) = > {
for (let start = Date.now(); Date.now() - start <= delay; ) {}};// All other logic remains unchanged
let performUnitOfWork = () = > {
taskQueue.shift()();
};
// ...
Copy the code
You can see that each time the browser executes a task, with 0ms remaining, it cedes control to the browser and waits for the next frame to execute the workloop method again.
But requestIdleCallback is currently only supported by some browsers, so React implemented a requestIdleCallback itself. It simulates deferring the callback until after the ‘draw operation’. The following is the main implementation process, and we will continue this idea with Fiber implementation later.
// 1. Define the unit of work for the next execution
let nextUnitOfWork = null
// 2. Define the callback function
function workLoop(deadline) {
/ / mark
let shouldYield = false;
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
}
// Apply for the next slice in advance
requestIdleCallback(workLoop)
}
// 3. Register a callback event with the browser and apply for a time slice
requestIdleCallback(workLoop)
// 4. Execute the unit of work and return to the next unit of work
function performUnitOfWork(nextUnitOfWork) {
// ...
}
Copy the code
React implements requestIdleCallback(). How does React split units of work?
I met Fiber
Fiber is a refactoring of the React core algorithm. The Facebook team spent more than two years refactoring the React core algorithm and introduced Fiber architecture in Version 16 and later.
Fiber is both a data structure and a unit of work
- Fiebr as data structure
The React Fiber mechanism relies on the following data structures – linked lists. Each of these nodes is a fiber. A fiber contains attributes such as child, sibling, and parent. The Fiber node also stores the type of the node, information about the node (such as state, props), and the corresponding value of the node.
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
Copy the code
- Fiber is the unit of work
Think of it as an execution unit, and after each execution of an “execution unit” (next Time of work below) React checks how much time is left and cedes control if it does not.
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// Determine whether the current frame has time left
}
Copy the code
In order to realize the Fiber structure, there are two steps: first, how to create the Fiber tree, and second, how to traverse the Fiber tree.
The idea behind React is to try to update components recursively to execute them in a linked list. So what we’re going to do is we’re going to transform our virtual DOM tree into a Fiber tree.
Specific Fiber implementation
Since it is the uninterruptible way recursive calls are made in the Render method that causes performance problems, let’s tune the Render method
// For the previous render method, just keep the logic for creating the node and rename it createDom.
function createDom(fiber) {
// 1. Create different types of DOM nodes
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
// 2. Add props (no children) to the DOM node
const isProperty = (key) = >key ! = ="children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach((name) = > {
dom[name] = fiber.props[name];
});
return dom;
}
Copy the code
The unit of work is then defined and initialized in the Render method.
let nextUnitOfWork = null;
function render(element, container) {
// Define initial unit of work (define initial Fiber root node)
nextUnitOfWork = {
dom: container, // root
props: {
children: [element], // DOM}};console.log("1. The initial Fiber", nextUnitOfWork);
}
Copy the code
If I print the fiber structure, I can see that the original fiber structure corresponds to the root node of the Fiber tree. The root node and props. Children in the DOM attribute store the initial virtual DOM structure (each fiber node in the fiber tree is created successively according to the complete virtual DOM structure).
Following our previous understanding of requestIdleCallback, let’s define an event loop and register the callback event in the requestIdleCallback() method.
function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() <1;
}
if (nextUnitOfWork) {
requestIdleCallback(workLoop);
}
}
requestIdleCallback(workLoop);
Copy the code
So this formUnitofwork () method, what does it do?
- Add elements to the DOM
- When creating a Fiber from the root Fiber down, we are always creating a Fiber for the child node (step-by-step process of creating a Fiber list)
- Traverse the Fiber tree to find the next unit of work (traverse the Fiber tree)
/** * what does the execution unit of work do ❓ */
function performUnitOfWork(fiber) {
// 1. Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber); }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// 2. When creating a Fiber from the root Fiber down, we always create a Fiber for the child nodes
const elements = fiber.props.children; // The previous vDOM structure
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
dom: null.parent: fiber,
};
// The first child is called child and the remaining children are called sibling
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
console.log("2. Fiber tree after each unit of work", fiber);
// Step 2 implements the process of creating a fiber tree 👆👆👆
// Step 3 follows to traverse the fiber process 👇👇👇
// 3. Traverse the Fiber tree to find the next unit of work
if (fiber.child) {
return fiber.child;
}
while (fiber) {
if (fiber.sibling) {
returnfiber.sibling; } fiber = fiber.parent; }}Copy the code
We can see that each time we execute the unit of work, we gradually improve the fiber structure, which contains the parent, child and sibling pointing of the current processing node.
Finally, get the root node of the page and render it on the page.
const container = document.getElementById("root");
MyReact.render(element, container);
Copy the code
Gold – Commit Commit
In the formUnitofwork above, we add elements directly to the DOM each time. One problem with this is that the browser can interrupt us at any time, leaving the user with an incomplete UI, so we need to make a change to allow all units of work to be done before we add all the DOM at once. In other words, react has different mechanisms at different stages,
- The Render phase is interruptible
- The Commit phase is not interruptible
Below we mark out the parts that need to be changed
function performUnitOfWork(fiber) {
// Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber); }// step1. Remove the part of DOM node submission and carry out unified submission later
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }
// Create a fiber structure for all the children of the element (no children skipped)
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,
dom: null.parent: fiber,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// Find the next unit of work (traversing fiber tree)
if (fiber.child) {
return fiber.child;
}
while (fiber) {
if (fiber.sibling) {
returnfiber.sibling; } fiber = fiber.parent; }}// step2. Save a working fiber tree wipRoot (work in progress root) and initialize it in render to commit the whole fiber tree
// Perform performUnitOfWork again every time nextUnitOfWork
let wipRoot = null;
function render(element, container) {
// Initialize to trace fiber's root node and assign it to nextUnitOfWork
wipRoot = {
dom: container,
props: {
children: [element],
},
};
nextUnitOfWork = wipRoot;
}
// After judging that all units of work have been executed, perform the "submit" operation
function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() <1;
}
// Perform the commit operation
if(! nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); }// 4. Create a commit function to add all elements to the DOM tree
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}
// Make a recursive commit
function commitWork(fiber) {
if(! fiber) {return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
Copy the code
Complete DEMO
You can see that the page has been rendered successfully!
React converts JSX elements into the familiar virtual DOM structure. How Fiber architecture can optimize and split units of work and realize asynchronous interruptible mechanism; And how to traverse a Fiber tree and submit it to the page for rendering.
Of course, the famous Reconciliation algorithm of React has not been mentioned in this article. It is the core mechanism for React update scheduling and greatly improves react performance. We will discuss it later.
Lin Clark’s presentation in ReactConf 2017 is a classic. It’s like a little person diving, if he dives and dives deeper and deeper, then he can’t sense what’s happening on the shore (the stack gets deeper and deeper and cannot be interrupted); The second diagram is much more flexible, as it is when you dive for a certain amount of time and go back to shore to see if there is a new task to be done (asynchronously interruptible, each time deciding if there is a higher priority task).
Of course, to quote Yulebrook, React Fiber is essentially a solution to React update inefficiencies. Don’t expect Fiber to improve your existing applications substantially. If performance issues are your own problem, you’ll have to deal with it yourself.
Finished! ~ Sow flowers, remember (° °) Blue ✿
reference
Build your own React
Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
React Technology revealed