This is an article on the React Fiber algorithm, which is easy to understand. Besides, the author has implemented the core code of Fiber himself, which can help us understand the link of the original text of Fiber
In addition, it is recommended to read his other react articles before reading this article. This article is a DIY react based on them
Didact Fiber: Incremental reconciliation
github repository updated demo
Why Fiber
This article will not show a complete React Fiber, if you want to learn more, more information
When the main thread of the browser is busy running something for a long time, the execution of critical tasks can be delayed.
To show this problem, I made a demo where the main thread needs to be called every 16ms to keep the planet moving because the animation runs on the main thread. If the main thread is occupied by something else, say 200ms, you will find that the animation freezes and the planet stops running until the main thread is free to run the animation.
What is it about the main thread that makes it so busy that it can’t spare a few microseconds to keep the animation flowing and responsive?
Remember the reconciliation Code implemented earlier? Once you start, you can’t stop. If the main thread needs to do something else at this point, it can only wait. And because you use a lot of recursion, it’s hard to pause. That’s why we rewrote the code to use loops instead of recursion.
Scheduling micro-tasks
We need to break the task into sub-tasks that run and finish in a very short period of time. You can have the main thread work on higher priority tasks first and then come back to work on lower priority tasks later.
We will need the help of the requestIdleCallback() function. The deadline parameter in the callback function tells you how much free time you have left to run the code. If there is not enough time left, you can choose not to execute the code, so that the main thread will not be used forever.
const ENOUGH_TIME = 1; // milliseconds
let workQueue = [];
let nextUnitOfWork = null;
function schedule(task) {
workQueue.push(task);
requestIdleCallback(performWork);
}
function performWork(deadline) {
if(! nextUnitOfWork) { nextUnitOfWork = workQueue.shift(); }while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork || workQueue.length > 0) { requestIdleCallback(performWork); }}Copy the code
The function that really works is formUnitofwork. We will write the Reconciliation code in it. The function takes very little time to run once and returns information about the next task.
To organize these subtasks, we’ll use Fibers
The fiber data structure
We will create a fiber for each component that needs to be rendered. NextUnitOfWork is a reference to the next fiber that will run. PerformUnitOfWork diff fiber and return to the next fiber. This will be explained in more detail later.
What does Fiber look like?
let fiber = {
tag: HOST_COMPONENT,
type: "div".parent: parentFiber,
child: childFiber,
sibling: null.alternate: currentFiber,
stateNode: document.createElement("div"),
props: { children: [].className: "foo"},
partialState: null.effectTag: PLACEMENT,
effects: []};Copy the code
It is an object, and we will use the parent, child, sibling attributes to construct the Fiber tree to represent the structure tree of the component.
A stateNode is a reference to a component instance. It may be a DOM element or a user-defined instance of a class component
Here’s an example:
In the example above we can see that three different components will be supported:
- B, P, and I stand for
host component
. We’ll define it with tag: HOST_COMPONENT. The type attribute will be a string. Props are DOM properties and events. - Foo class component. Its tag: CLASS_COMPONENT, type refers to the user-defined class component
- Div stands for host root. It is similar to host Component, and the stateNode is DOM element.tag: HOST_ROOT. Note that stateNode is the argument passed to the Render function.
Another important property is alternate, which we need because most of the time we will have two Fiber trees. One represents a rendered DOM, which we call a current tree or an old tree. The other one is created when updating (when setState or Render is called) and is called work-in-Progress Tree.
The work-in-Progress tree does not share any fiber with the old tree. Once we finish building the work-in-Progress tree and changing the DOM, the work-in-Progress tree becomes an old tree.
So we use alternate property to link to the old tree. Fiber has the same Tag, Type, statenode as its alternate. Sometimes we render a new component that may have no alternate property
Then, there’s a list of effects and an effectTag. When we find a DOM that work-in-progress needs to change, we set effectTags to PLACEMENT, UPDATE, and DELETION. To make it easier to see which of the total fibers needed to change the DOM, we put all fibers in the Effects list.
There’s probably a lot of conceptual stuff here, but don’t worry, we’ll show Fiber in action.
Didact call hierarchy
To get a general understanding of the program, let’s first look at the structure diagram
We’ll start with render() and setState() and end with commitAllWork()
Old code
I told you we’d be refactoring most of the code, but before we do that, let’s review the code that doesn’t need refactoring
I’m not going to translate all of this, but this is the code I mentioned at the beginning of this article
- Element creation and JSX
- Instances, reconciliation and virtual DOM
- Components and State, the Class Component needs to be changed slightly
class Component {
constructor(props) {
this.props = props || {};
this.state = this.state || {};
}
setState(partialState) {
scheduleUpdate(this, partialState); }}function createInstance(fiber) {
const instance = new fiber.type(fiber.props);
instance.__fiber = fiber;
return instance;
}
Copy the code
render() & scheduleUpdate()
In addition to Component and createElement, we’ll have two public functions render() and setState(). We’ve already seen that setState() only calls scheduleUpdate().
Render () and scheduleUpdate() are very similar in that they receive new updates and queue.
/ Fiber tags
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";
// Global state
const updateQueue = [];
let nextUnitOfWork = null;
let pendingCommit = null;
function render(elements, containerDom) {
updateQueue.push({
from: HOST_ROOT,
dom: containerDom,
newProps: { children: elements }
});
requestIdleCallback(performWork);
}
function scheduleUpdate(instance, partialState) {
updateQueue.push({
from: CLASS_COMPONENT,
instance: instance,
partialState: partialState
});
requestIdleCallback(performWork);
}
Copy the code
We will use the Update Ue array to store pending updates. Each call to Render or scheduleUpdate stores data into update Ue. Each data in the array is different and we will use it in the resetNextUnitOfWork() function.
After storing the data push into the queue, we will call performWork() asynchronously.
performWork() && workLoop()
const ENOUGH_TIME = 1; // milliseconds
function performWork(deadline) {
workLoop(deadline);
if (nextUnitOfWork || updateQueue.length > 0) { requestIdleCallback(performWork); }}function workLoop(deadline) {
if(! nextUnitOfWork) { resetNextUnitOfWork(); }while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if(pendingCommit) { commitAllWork(pendingCommit); }}Copy the code
This uses the FormUnitofwork pattern we saw earlier.
WorkLoop () determines whether the deadline has enough time to run the code. If not, stop the loop and return to performWork(). NextUnitOfWork is still reserved for the next task, and determine whether it needs to be executed in performWork().
PerformUnitOfWork () builds the Work-in-Progress tree and finds out which DOM changes need to be manipulated. This is done incrementally, one fiber at a time.
If performUnitOfWork() does all the work for this update, the renturn value is null and commitAllWork is called to change the DOM.
So far, we haven’t seen how the first nextUnitOfWork was created
resetUnitOfWork()
The function takes the first update value item and converts it to Next Value of work.
function resetNextUnitOfWork() {
const update = updateQueue.shift();
if(! update) {return;
}
// Copy the setState parameter from the update payload to the corresponding fiber
if (update.partialState) {
update.instance.__fiber.partialState = update.partialState;
}
const root =
update.from == HOST_ROOT
? update.dom._rootContainerFiber
: getRoot(update.instance.__fiber);
nextUnitOfWork = {
tag: HOST_ROOT,
stateNode: update.dom || root.stateNode,
props: update.newProps || root.props,
alternate: root
};
}
function getRoot(fiber) {
let node = fiber;
while (node.parent) {
node = node.parent;
}
return node;
}
Copy the code
If the update contains partialState, it will be stored on the corresponding fiber, which will later be assigned to the component instance, already available to Render.
Then, we find the root node of the old Fiber tree. If update is called by First Render, root fiber will be null. For render, root would be _rootContainerFiber. If update is due to setState(), go up to the first fiber without the patient attribute.
We then assign it to nextUnitOfWork, and note that this fiber will be the root element of work-in-Progress.
If there is no old root. The stateNode takes the arguments in Render (). Props will be the other argument to Render (). Children in props is an array. Alternate is null.
If I have old root. StateNode is the former root DOM node. Props will be newProps if it is not null, otherwise it is the original props. Alternate is old root before.
Now that we have the root element for work-in-Progress, let’s construct the rest
performUnitOfWork()
function performUnitOfWork(wipFiber) {
beginWork(wipFiber);
if (wipFiber.child) {
return wipFiber.child;
}
// No child, we call completeWork until we find a sibling
let uow = wipFiber;
while (uow) {
completeWork(uow);
if (uow.sibling) {
// Sibling needs to beginWork
returnuow.sibling; } uow = uow.parent; }}Copy the code
PerformUnitOfWork () traverses the work-in-progress tree
The beginWork() function is to create the fiber of the child node. And the first child is the child attribute of fiber
If currently fiber has no children, we call completeWork() and return Sibling as the next nextUnitOfWork.
If there is no Sibling, proceed up the Parent Fiber. Until the root.
In general, the leaf node is processed first, then its sibling, then its parent. Go from the bottom up.
beginWork(), updateHostComponent(), updateClassComponent()
unction beginWork(wipFiber) {
if (wipFiber.tag == CLASS_COMPONENT) {
updateClassComponent(wipFiber);
} else{ updateHostComponent(wipFiber); }}function updateHostComponent(wipFiber) {
if(! wipFiber.stateNode) { wipFiber.stateNode = createDomElement(wipFiber); }const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
function updateClassComponent(wipFiber) {
let instance = wipFiber.stateNode;
if (instance == null) {
// Call class constructor
instance = wipFiber.stateNode = createInstance(wipFiber);
} else if(wipFiber.props == instance.props && ! wipFiber.partialState) {// No need to render, clone children from last time
cloneChildFibers(wipFiber);
return;
}
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
const newChildElements = wipFiber.stateNode.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
Copy the code
BeginWork () has two functions
- Create stateNode
- Get the Component Children and call
reconcileChildrenArray()
Because different types of Components are handled differently, there are two functions: updateHostComponent and updateClassComponent.
UpdateHostComponennt handles the Host component and root Component. If fiber doesn’t have a DOM node, create one (just create a DOM node, no child nodes, and no inserts into the DOM). Then use the Children in Fiber props to call the reconcileChildrenArray()
UpdateClassComponent handles the user-created class Component. If there is no instance, create one. And updated props and state so that render can calculate the new children.
UpdateClassComponent does not call the Render function every time. This is somewhat similar to shouldCompnentUpdate. If render is not needed, the child nodes are copied.
Now that we have newChildElements, we are ready to create child Fiber.
reconcileChildrenArray()
Notice, this is the core. Here you create the work-in-Progress tree and decide how to update the DOM
/ Effect tags
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;
function arrify(val) {
return val == null ? [] : Array.isArray(val) ? val : [val];
}
function reconcileChildrenArray(wipFiber, newChildElements) {
const elements = arrify(newChildElements);
let index = 0;
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber = null;
while(index < elements.length || oldFiber ! =null) {
const prevFiber = newFiber;
const element = index < elements.length && elements[index];
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
tag: oldFiber.tag,
stateNode: oldFiber.stateNode,
props: element.props,
parent: wipFiber,
alternate: oldFiber,
partialState: oldFiber.partialState,
effectTag: UPDATE
};
}
if(element && ! sameType) { newFiber = {type: element.type,
tag:
typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
effectTag: PLACEMENT
};
}
if(oldFiber && ! sameType) { oldFiber.effectTag = DELETION; wipFiber.effects = wipFiber.effects || []; wipFiber.effects.push(oldFiber); }if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index == 0) {
wipFiber.child = newFiber;
} else if(prevFiber && element) { prevFiber.sibling = newFiber; } index++; }}Copy the code
First we make sure newChildElements is an array (unlike the previous diff algorithm, the children of this algorithm are always arrays, which means we can return arrays in render)
Then, start comparing the Children in Old Fiber to the new Elements. Remember? Alternate is old fiber. New elements come from props. Children (function) and Render (Class Component).
Reconciliation algorithm firstly diff wipFiber. Alternate. Child and elements [0], then wipFiber. Alternate. Child. (and elements [1]. And you keep doing that until you’re done.
- if
oldFiber
andelement
It has the same type. I’m going to create a new one from old Fiber. Notice the increaseUPDATE effectTag
- If the two have different types or no corresponding oldFiber (because we added a new child node), create a new fiber. Note that new Fiber is not coming
alternate
Attribute and stateNode (stateNode will be inbeginWork()
Created in). Also addedPLACEMENT effectTag
. - If the two have different types or no corresponding
element
(Because we removed some child nodes). Let’s label old fiberDELETION
.
cloneChildFibers()
There is a special case in updateClassComponent where render is not required and fiber is copied directly.
function cloneChildFibers(parentFiber) {
const oldFiber = parentFiber.alternate;
if(! oldFiber.child) {return;
}
let oldChild = oldFiber.child;
let prevChild = null;
while (oldChild) {
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber
};
if (prevChild) {
prevChild.sibling = newChild;
} else{ parentFiber.child = newChild; } prevChild = newChild; oldChild = oldChild.sibling; }}Copy the code
CloneChildFibers () copies all the subfibers of the old fiber. We don’t need to add effectTags because we’re sure we don’t need to change anything.
completeWork()
PerformUnitOfWork, we call completeWork when there are no new children in wipFiber, or we have processed all the children.
function completeWork(fiber) {
if (fiber.tag == CLASS_COMPONENT) {
fiber.stateNode.__fiber = fiber;
}
if (fiber.parent) {
const childEffects = fiber.effects || [];
constthisEffect = fiber.effectTag ! =null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
} else{ pendingCommit = fiber; }}Copy the code
In completeWork, we create a new list of Effects. EffecTag contains all of the included effecTag in work-in-Progress. Convenient for later processing. Finally, we pointed the pendingCommit to root Fiber. And used in workLoop.
commitAllWork & commitWork
That’s the last thing we need to do, change the DOM.
function commitAllWork(fiber) {
fiber.effects.forEach(f= > {
commitWork(f);
});
fiber.stateNode._rootContainerFiber = fiber;
nextUnitOfWork = null;
pendingCommit = null;
}
function commitWork(fiber) {
if (fiber.tag == HOST_ROOT) {
return;
}
let domParentFiber = fiber.parent;
while (domParentFiber.tag == CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.stateNode;
if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
domParent.appendChild(fiber.stateNode);
} else if (fiber.effectTag == UPDATE) {
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if(fiber.effectTag == DELETION) { commitDeletion(fiber, domParent); }}function commitDeletion(fiber, domParent) {
let node = fiber;
while (true) {
if (node.tag == CLASS_COMPONENT) {
node = node.child;
continue;
}
domParent.removeChild(node.stateNode);
while(node ! = fiber && ! node.sibling) { node = node.parent; }if (node == fiber) {
return; } node = node.sibling; }}Copy the code
CommitAllWork first iterates through all root Root effects.
- PLACEMENT. Insert the DOM into the parent node
- The UPDATE. Give the old and new props
updateDomProperties()
To deal with. - DELETION. If it’s the Host Component. Just remove it with removeChild(). If it is class Component, remove all host Components under fiber subTree.
Once we’ve done all the effects, reset nextUnitOfWork and pendingCommit. The work-in-Progress tree becomes the Old tree. Copy it to _rootContainerFiber. So we’re done with the update and ready to wait for the next update.
See my home page or blog for more articles