The biggest difference between before and after React 16 is that 16 introduces fiber and implements hooks based on fiber. I mention fiber all day. What is fiber? What is its relationship with vdom?
Instead of looking at various explanations, it's better to write a fiber version of React. When you can realize it, you must fully understand it.
vdom and fiber
First, we use vdom to describe the interface structure, for example:
{ "type": "ul", "props": { "className": "list", "children": [ { "type": "li", "props": { "className": "item", "children": [ "aa" ] } }, { "type": "li", "props": { "className": "item", "children": [ "bb" ] } } ] } }
This is obviously a ul and li structure. But instead of writing vdom directly, we will use jsx:
const data = { item1: 'bb', item2: 'cc' } const jsx = <ul className="list"> <li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aa</li> <li className="item">{data.item1}<i>xxx</i></li> <li className="item">{data.item2}</li> </ul>;
jsx is compiled with babel. Let's configure it babelrc.js:
module.exports = { presets: [ [ '@babel/preset-react', { pragma: 'Dong.createElement' } ] ] }
Then compile it with babel:
babel index.js -d ./dist
The compilation result is as follows:
const data = { item1: 'bb', item2: 'cc' }; const jsx = Dong.createElement("ul", { className: "list" }, Dong.createElement("li", { className: "item", style: { background: 'blue', color: 'pink' }, onClick: () => alert(2) }, "aa"), Dong.createElement("li", { className: "item" }, data.item1, Dong.createElement("i", null, "xxx")), Dong.createElement("li", { className: "item" }, data.item2));
The createElement here is called render function, and its execution result is vdom.
Why not compile jsx directly into vdom?
Because render function can execute dynamic logic. We can add state and props, or wrap the implementation components.
In this way, we just need to realize Dong CreateElement will get vdom:
createElement is the object that returns type, props and children.
We also put children in props, and the text node is created separately:
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: [], }, } } const Dong = { createElement }
In this way, vdom will be rendered:
I printed it:
Next, recursive rendering of this vdom tree is just rendering, that is, through document CreateElement creates elements, sets attributes, styles, event listeners, and so on.
Wait, if this is done, it is the architecture before React 16. We have achieved this:
Handwriting simple front-end framework: vdom rendering and jsx compilation
Handwriting simple front-end framework: function and class components
Handwriting simple front-end framework: vdom rendering and jsx compilation
The fiber architecture was introduced after React 16, which was changed here. Instead of directly rendering vdom, it was first converted to fiber:
Originally, in vdom, parent-child nodes are associated through children, while in fiber, the first child node is associated through children, and then the next node is connected in series through sibling. All nodes can return to the parent node.
In this way, a vdom tree is turned into a fiber linked list?
Then you can render fiber, just like when rendering vdom.
Why does it take so much work to turn it into another structure and render it? Isn't that unnecessary?
That's definitely not true. The meaning of fiber architecture is here:
Previously, we rendered vdom recursively, and then diff came down to render patch:
This rendering and diff are done recursively.
Now it's like this:
First transfer vdom to fiber, that is, the process of reconcile. Because fiber is a linked list, it can be interrupted. Use schedule to schedule the requestIdleCallback. Finally, after all the transfers are completed, render again at one time. This process is called commit.
In this way, there were only render and patch of vdom before, but now it has become the reconcile of vdom to fiber, the scdule of idle scheduling reconcile, and finally the commit of fiber rendering.
The significance lies in this interruptibility. Because recursive rendering vdom may take a lot of time, a large amount of JS calculation will block rendering, while fiber is interruptible, it will not block rendering. In addition, dom that needs to be used will be created and diff will be made to determine whether to add, delete or change.
With dom, you know how to add, delete and change it. One time commit will be fast.
This is the meaning of fiber architecture!
Next, let's implement it.
Realize fiber version react
Let's do it from top to bottom, that is, implement schedule, reconcile and commit respectively
schedule
Schedule is idle scheduling, which is like this:
function workLoop(deadline) { // do xxx requestIdleCallback(workLoop); } requestIdleCallback(workLoop);
It is a continuous loop, just like event loop, which can be called reconcile loop.
Then what it does is convert vdom to fiber, that is, reconcile:
We use two global variables to record the currently processed fiber node and root fiber node:
let nextFiberReconcileWork = null; let wipRoot = null;
What it does is loop through all reconcile s:
let shouldYield = false; while (nextFiberReconcileWork && !shouldYield) { nextFiberReconcileWork = performNextWork( nextFiberReconcileWork ); shouldYield = deadline.timeRemaining() < 1; }
If there is a next fiber and there is still free time, execute the reconcile of the next vdom to fiber
If all of them have been transferred, commit:
if (!nextFiberReconcileWork) { commitRoot(); }
Therefore, the code of schedule is like this:
let nextFiberReconcileWork = null; let wipRoot = null; function workLoop(deadline) { let shouldYield = false; while (nextFiberReconcileWork && !shouldYield) { nextFiberReconcileWork = performNextWork( nextFiberReconcileWork ); shouldYield = deadline.timeRemaining() < 1; } if (!nextFiberReconcileWork) { commitRoot(); } requestIdleCallback(workLoop); } requestIdleCallback(workLoop);
The performNextWork executed each time is reconcile:
function performNextWork(fiber) { reconcile(fiber); if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } }
reconcile the current fiber node, and then continue to process child and sibling in order. After processing, return to the fiber node of return.
Such continuous scheduling reconcile.
This is what schedule does: schedule is to schedule reconciles (vdom to fiber) of each fiber node through idle, and execute commit after all reconciles are completed.
Next, implement reconcile:
reconcile
Schedule's loop is already in progress, so as long as you submit a nextfiberrecipilework, you can handle it in the next loop.
Therefore, this is the implementation of render:
function render(element, container) { wipRoot = { dom: container, props: { children: [element], } } nextFiberReconcileWork = wipRoot }
Create a root fiber node and assign it to wipRoot, which means fiber root of working in progress. And the next processing fiber node points to it, so the next schedule will schedule this fiber node to start reconciling.
reconcile is the conversion from vdom to fiber, but it also does two things: one is to create the corresponding dom node in advance, and the other is to do diff to determine whether to add, delete or change.
The implementation of reconcile is as follows:
function reconcile(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }
fiber.props.children are the child nodes of vdom. The reconcileChildren here is a fiber linked list that transforms the previous vdom into children, sibling s and return:
Loop through the elements of each vdom. If the index is 0, it means that the child is connected in series, otherwise it is connected in series. All created nodes should point to the parent node with return:
function reconcileChildren(wipFiber, elements) { let index = 0 let prevSibling = null while ( index < elements.length ) { const element = elements[index] let newFiber = { type: element.type, props: element.props, dom: null, return: wipFiber, effectTag: "PLACEMENT", } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } }
Because we only realize rendering, and do not do diff and delete modification for the time being, the effectTag here is placement, that is, new elements.
The whole fiber linked list can be generated by processing each vdom to fiber through schedule idle scheduling.
Therefore, this is what reconcile does: reconcile is responsible for converting vdom to fiber, and will also prepare the dom nodes to be used, determine whether to add, delete or change, and finally convert the whole vdom tree into fiber linked list through schedule scheduling.
When the fiber is finished, the schedule will enter here:
if (!nextFiberReconcileWork) { commitRoot(); }
Start commit:
commit
commit is the addition, deletion and modification of dom, which is faster than the rendering in the previous vdom architecture. Because the dom is created in advance and you know whether to add, delete or change, isn't the rest very simple?
We start the commit from the root fiber and set wipRoot to null, because we no longer need to schedule it:
function commitRoot() { commitWork(wipRoot.child); wipRoot = null }
The rendering of each fiber node is inserted into the dom in the order of child and sibling:
function commitWork(fiber) { if (!fiber) { return } let domParentFiber = fiber.return while (!domParentFiber.dom) { domParentFiber = domParentFiber.return } const domParent = domParentFiber.dom if ( fiber.effectTag === "PLACEMENT" && fiber.dom != null ) { domParent.appendChild(fiber.dom) } commitWork(fiber.child) commitWork(fiber.sibling) }
Here, each fiber node needs to find its parent node, because we just add it, so we only need appendChild.
dom has been created in the reconcile node. We didn't elaborate at that time. Now let's look at the dom creation logic:
function createDom(fiber) { const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type); for (const prop in fiber.props) { setAttribute(dom, prop, fiber.props[prop]); } return dom; }
To create an element based on its type and set its attributes:
Attribute to handle style, value of text node and event listener respectively:
function isEventListenerAttr(key, value) { return typeof value == 'function' && key.startsWith('on'); } function isStyleAttr(key, value) { return key == 'style' && typeof value == 'object'; } function isPlainAttr(key, value) { return typeof value != 'object' && typeof value != 'function'; } const setAttribute = (dom, key, value) => { if (key === 'children') { return; } if (key === 'nodeValue') { dom.textContent = value; } else if (isEventListenerAttr(key, value)) { const eventType = key.slice(2).toLowerCase(); dom.addEventListener(eventType, value); } else if (isStyleAttr(key, value)) { Object.assign(dom.style, value); } else if (isPlainAttr(key, value)) { dom.setAttribute(key, value); } };
This is done in reconcile, and the commit is naturally fast.
This is what commit does: add the fiber linked list generated by reconcile to the dom at one time, because the node corresponding to fiber is created in advance, and you know whether to add, delete or change. Therefore, this stage is very fast.
In this way, we have realized the simple version of React. Of course, at present, we only realize rendering. Let's try the effect:
Such a paragraph jsx:
const data = { item1: 'bb', item2: 'cc' } const jsx = <ul className="list"> <li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aa</li> <li className="item">{data.item1}<i>xxx</i></li> <li className="item">{data.item2}</li> </ul>; console.log(JSON.stringify(jsx, null, 4)); Dong.render(jsx, document.getElementById("root"));
After rendering, it is like this:
The code was uploaded to GitHub: https://github.com/QuarkGluonPlasma/frontend-framework-exercize
summary
Fiber is an architecture change introduced by React16. In order to fully understand it, we have implemented a simple version of fiber architecture React.
The interface is described by vdom, but it is not written by hand, but generated after the render function generated by jsx compilation. In this way, state, props and some dynamic logic can be added to dynamically generate vdom.
After vdom is generated, it is no longer directly rendered, but first converted to fiber. This process of vdom to fiber is called reconcile.
Fiber is a linked list structure, which can be interrupted. In this way, the reconcile can be idle scheduled through requestIdleCallback. In this way, the cycle continues until all the reconciles of vdom to fiber are processed, and then the commit starts, that is, the update to dom.
The process of reconcile will create the dom in advance and mark the addition, deletion and modification, so the commit phase will be very fast.
From the previous recursive rendering, diff is used to determine the addition, deletion, modification and creation of dom. It is advanced to the interruptible reconcile stage, which makes the commit very fast. This is the purpose and significance of fiber architecture.
Of course, we haven't implemented hooks and update and deletion yet, which will be implemented in the future.
If you want to thoroughly understand the fiber architecture, you might as well implement the reconcile process according to the article, which will give you a deeper understanding of it.