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
-
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
-
Create the following directory structure
- src/ - myReact/ - index.js - index.html - main.jsx Copy the code
-
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
-
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 of
plugin-transform-react-jsx
What you do is simple: useReact.createElement
Function to handle JSX syntax from. JSX files. - That’s why in.jsx files you have to
import 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.
- 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.
- 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 use
requestIdleCallback
Let’s do a loop. It can berequestIdleCallback
As an asynchronous task, the browser will run the callback when the main thread is idle, rather than telling us when to run it. requestIdleCallback
We 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 one
performUnitOfWork
Function. 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
- Static data structures (virtual DOM)
- As schema: Connect parent, child, and sibling nodes
- As a unit of work
Fiber Tree Organizational form
- Create one in Render
rootFiber
Node, and make it the firstNextUnitOfWork (an Instance of Fiber)
The incoming performUnitOfWork
acceptnextUnitOfWork
As parameters and do three things:- Add the corresponding fiber node to the DOM
- Create a subfiber node of the fiber node
- Select the next unit of work
The purpose of this data structure is to make it easier to find the next unit of work:
- After the current work on Fiber is completed, if
fiber.child! ==null
,fiber.child
The node will be the next unit of work. - The current Fiber has no child nodes, then
fiber.sibling! ==null
In the case of,fiber.sibling
The node will be the next unit of work. - Current Fiber node
fiber.child===null && fiber.sibiling===null
In the case of,fiber.parent
The node’ssibling
The node will be the next unit of work. - 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:
Fiber
nodesFiber.dom
nullchildren
You 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.
-
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
-
-
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.
-