review
To create a React app, we need the following three lines of code
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
In the first line we define an element using JSX syntax, which is converted to JS via Babel and other build tools. The conversion is usually simple, passing the label name, attribute, and child elements as parameters to the createElement function.
(1)
const element = <h1 title="foo">Hello</h1>
Copy the code
Is equivalent to
Const element = react. createElement("h1", // tag {title: "foo"}, // Element's attribute value object "Hello" // element's child node)Copy the code
(2)
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
Copy the code
Is equivalent to
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
Copy the code
1. The createElement method function
function createElement(type, props, ... children) { return { type, props: { ... props, children, }, } }Copy the code
The createElement method (” div “) will return
{
"type" : "div",
"props":{"children" : []}
}
Copy the code
The createElement method (” div “, null, a, b) returns
{
"type" : "div",
"props":{"children" : [a,b]}
}
Copy the code
Subarrays can also contain strings, numbers, etc. So we’ll wrap everything that isn’t an object in its own element and create a special type for them: TEXT_ELEMENT.
function createElement(type, props, ... children) { return { type, props: { ... props, children: children.map(child => typeof child === "object" ? child : createTextElement(child) ), }, } } function createTextElement(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [], }, } }Copy the code
In React, we don’t wrap raw values or create empty arrays without child elements, but we do this to simplify the code.
Const MyReact = {createElement,} /** @jsx MyReact. CreateElement */ (when Babel translates JSX, it will use MyReact instead of React.) const element = MyReact.createElement( "div", { id: "foo" }, MyReact.createElement("a", null, "bar"), MyReact.createElement("b") )Copy the code
2. The Render function
function render(element, container) { // 1. Const dom = element.type === "TEXT_ELEMENT"? document.createTextNode("") : document.createElement(element.type); // 2. Assign object.keys (element.props).filter((item) => item! == "children") .forEach((name) => { dom[name] = element.props[name]; }); / / 3. Recursive node element. Props. Children. The forEach ((child) = > render (child, dom)); // 4. Insert dom container. AppendChild (dom); }Copy the code
Render uses recursive traversal above, which can be a bit problematic. Once render is executed, the render function does not end until the DOM tree is rendered. If the DOM tree is very large, it might block the main thread for too long. If the browser needs to handle high-priority work such as user input or smooth animations, it must wait until the rendering is complete.
3. Parallel mode
Ideally, we should break render into more subdivided units, allowing the browser to interrupt the higher-priority work of rendering response after each unit is completed, a process known as “concurrent mode”.
Here we use the browser API requestIdleCallback. This API is similar to setTimeout, except that instead of telling the browser when to execute the callback, the browser actively executes the callback while the thread is idle.
React no longer uses this API due to issues with requestIdleCallback (not executing frequently enough for smooth UI rendering, compatibility, etc.). React now uses the scheduler/Scheduler package to implement its own scheduling algorithm. But their core ideas are similar, and for the sake of convenience this demo uses requestIdleCallback.
Let nextUnitOfWork = null function workLoop(deadline) {shouldYield = false while (nextUnitOfWork &&! ShouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork); ShouldYield = deadline. TimeRemaining () < 1} requestIdleCallback(workLoop)} // The task loop entry requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork) { // TODO }Copy the code
To start the loop, we need to set up the first unit of work, then write a function that performs the work and returns the next unit of work. performUnitOfWork
4.Fibers
To assign units of work, we need a data structure: FIber tree. Each element has a fiber, and each fiber corresponds to a unit of work.
- Every element has an attribute that points to its parent (except the root element)
- Each element has an attribute that points to its first child (if it has a son)
- Each element has attributes that point to its neighboring sibling element (if any)
One of the goals of this data structure is to quickly find the next unit of work. Each fiber is linked to its first child, its next sibling, and its parent.
When a fiber completes work, if it has a child node, the child node becomes the next unit of work.
If a fiber has no children and no siblings, its parent is returned. Until we find the root node, which means we’ve done all the work to perform this rendering
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
Copy the code
In rendering, we will create root Fiber and set it to work first. The rest of the work will be inperformUnitOfWork
On each fiber, we’re going to do three things:
- Add elements to the DOM
- Create fiber for the child element
- Go to the next unit of work
Refactoring previous code
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 } function render(element, container) { // TODO set next unit of work } let nextUnitOfWork = nullCopy the code
In the Render function, we set nextUnitOfWork as the root node of the Fiber tree.
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
Copy the code
Then, when the browser is ready, our workLoop function is called to start work on the root node.
function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } Function performUnitOfWork(fiber) {// TODO add dom node // TODO create new fibers // TODO return next unit of work }Copy the code
5.Render and Commit phases
(1) The last section talked about using the free time to execute each unit task in order to create the Fiber model, so when the entire Fiber virtual tree is built, it is time to do the actual DOM rendering.
So the question is, how do we know when the virtual tree is built and when we’re ready to render it?
Starting with a review of the Fiber model and the performUnitOfWork function, you can see that the process of building a virtual tree is similar to DFS (depth-first traversal). It traverses from the root to the bottom and back again until it returns to the root. Since the root node has no siblings and no parent elements, the build is complete (performUnitOfWork() returns undefined).
At this point we’ll change the render function again and declare a variable wipRoot to store the root node of the virtual tree under construction.
(2) The browser may interrupt our work before we finish working on the whole tree. In this case, the user will see an incomplete UI. So, we need to delete the DOM changes.
/ / delete the following part of performUnitOfWork if (fiber. The parent) {fiber. The parent. The dom. The appendChild (dom) fiber.}Copy the code
Instead, we need to save the root of the Fiber tree, which we call Progress root or wipRoot.
let nextUnitOfWork = null
let wipRoot = null
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
Copy the code
When we’re done (and done when there’s no next unit of work), we commit the entire Fiber tree to the DOM together.
function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } if (! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }Copy the code
We implement this functionality in the commitRoot function, recursively adding nodes to the DOM.
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
6.Reconciliation
At this point, we’ve done our initial rendering work. What about updating the render again? We need to consider updating the DOM and deleting the DOM.
First of all, we don’t need to completely build a new DOM tree to render again, we can compare the current virtual tree with the virtual tree to render, and only need to change or delete elements to minimize unnecessary rendering.
So we need variables to record the current virtual tree and establish a relationship with the virtual tree to be rendered
Let nextUnitOfWork = null // Current virtual root node let currentRoot = null let wipRoot = null function commitRoot() { commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null } function render(element, container) { wipRoot = { dom: container, props: { children: [element], }, alternate: currentRoot, } nextUnitOfWork = wipRoot } let currentRoot = nullCopy the code
The code is extracted from performUnitOfWork to create the new Fiber, which is extracted into the new reconcileChildren function
function performUnitOfWork(fiber) { if (! Fiber. Dom) {fiber. Dom = createDom(fiber)} const elements = fiber. elements) if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } }Copy the code
Here, the old fiber is compared with the new one by position.
- Update an element of the same type in the same position.
- If the element type is different but the location is the same, add is added.
- If the old fiber has an element in the same position and the new fiber does not, delete the element.
function reconcileChildren(wipFiber, elements) { let index = 0; let oldFiber =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) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", }; } if (element && ! sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", }; } if (oldFiber && ! sameType) { oldFiber.effectTag = "DELETION"; deletions.push(oldFiber); } if (oldFiber) { oldFiber = oldFiber.sibling; } if (index === 0) { wipFiber.child = newFiber; } else if (element) { prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}Copy the code
Since the old fibers that need to be deleted do not need to be put back into the virtual tree, they are stored separately with deletions array variables, and the corresponding DOM is removed through the number group during subsequent rendering.
Deletions also need to go into other functions.
// Let deletions = null function render(element, container) {wipRoot = {dom: container, props: {children: props: [element],}, alternate: currentRoot,} // deletions = [] nextUnitOfWork = wipRoot}Copy the code
Then, when we commit to the DOM, we’ll use fiber in this array as well.
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
Copy the code
Now, let’s modify the commitWork function to handle the new effectTag attribute
function commitWork(fiber) { if (! Fiber) {return} const domParent = fiber.parent.dom // domparent.appendChild (fiber.dom) commitWork(fiber.sibling) }Copy the code
When fiber’s effectTag is PLACEMENT, we perform the same operation as before, adding the DOM node under the parent DOM.
if ( fiber.effectTag === "PLACEMENT" && fiber.dom ! = null ) { domParent.appendChild(fiber.dom) }Copy the code
In the case of DELETION, we do the opposite, deleting the DOM node from under the parent DOM.
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
Copy the code
When it is an UPDATE, we need to UPDATE the existing DOM node using the new props.
else if ( fiber.effectTag === "UPDATE" && fiber.dom ! = null ) { updateDom( fiber.dom, fiber.alternate.props, fiber.props ) }Copy the code
We’ll do that in the updateDom function.
function updateDom(dom, prevProps, nextProps) {
// TODO
}
Copy the code
We compared the old fiber with the new fiber props, deleted the props that were no longer used, and set the new or changed props. There are two types of props, one for on-starting events and the other for generic properties.
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) { //Remove old or changed event listeners 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]); }); // Remove old properties Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach((name) => { dom[name] = ""; }); // Set new or changed properties Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach((name) => { dom[name] = nextProps[name]; }); // Add event listeners Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach((name) => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); }Copy the code
7.Function Components
Let’s go back to the createElement section
Const element = (<input value="todo" />) const element = createElement('input', {value: Const element = {type: 'input', props: {value: 'todo', children: [],}}Copy the code
What if you write JSX as a function component
function App(props) { return <h1>Hi {props.name}</h1> }
const element = <App name="foo" />
Copy the code
According to the previous rules, it will also be converted
function App(props) {
return Didact.createElement(
'h1',
null,
'Hi ',
props.name
)
}
const element = Didact.createElement(App, {
name: 'foo',
})
const element = {
type: App,
props: {
name: 'foo',
children: [
{
type: 'h1',
props: {
children: [
{
type: 'TEXT_ELEMENT',
props: { 'nodeValue': 'Hi ', 'children': [] }
},
{
type: 'TEXT_ELEMENT',
props: { 'nodeValue': 'foo', 'children': [] }
}
]
}
}
]
}
}
Copy the code
What’s special here is that element’s attribute type is no longer a string of tag types, but a function. So the previous code needs to be changed again.
Function App() {return (<span>foo</span>)} const element = (<div id="root"> <App /> </div>) // fiber model div --> App <span>foo</span> </div>Copy the code
Based on these two points, part of the code in the performUnitOfWork function needs to be refactored.
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
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;
}
}
Copy the code
function performUnitOfWork(fiber) { const isFunctionComponent = fiber.type instanceof Function; if (isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); }... }Copy the code
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
Copy the code
Currently, on commitWork, the DOM is retrieved via fiber.parent, but fiber in a functional component does not have the DOM.
(1) First, we need to look up the Fiber tree until we find the fiber with the DOM node
(2) Second, when deleting a node, you need to find a child node with a DOM node.
function commitWork(fiber) {
if (!fiber) {
return;
}
// (1)修改
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
// (2)修改
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
Copy the code
(1)
let domParentFiber = fiber.parent; while (! domParentFiber.dom) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.dom;Copy the code
(2)
commitDeletion(fiber, domParent); function commitDeletion(fiber, domParent) { if (fiber.dom) { domParent.removeChild(fiber.dom); } else { commitDeletion(fiber.child, domParent); }}Copy the code
8.Hooks
function useState(initial) {
// TODO
}
Copy the code
We declare two variables, wipFiber and hookIndex, to store the fiber under construction and record the hook position, respectively.
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
Copy the code
function useState(initial) { const oldHook = wipFiber? .alternate? .hooks[hookIndex] const hook = { state: oldHook ? State: initial,} // wipfibre.hooks. Push (hook) hookIndex++ return [hook.state]}Copy the code
Here a hook uses an array to store state, and each time a hook is used, the array moves forward. Therefore, the position of the old hook and the new hook should be placed one by one, so that the new hook can accurately depend on the state of the old hook. React needs to specify rules for using hooks.
Currently only state is returned using useState, and setState needs to be added
function useState(inital) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : inital, queue: [], } const actions = oldHook ? oldHook.queue : [] actions. ForEach (action => {const isFunction = action instanceof Function // update state hook. State = isFunction? action(hook.state) : Action}) const setState => {hook. Queue. Push (action) // wipRoot = {dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * return [hook.state, setState] }Copy the code