A.

Fiber is a reconstruction of the React core algorithm, and the result of the two-year reconstruction is Fiber Reconciler

Core goal: Expand its applicability to include animation, layout and gestures. Divided into 5 specific goals (the last two are included) :

  • Break down interruptible work into smaller tasks

  • Prioritize what you’re doing, redo, and reuse what you did last time (half done)

  • Yield back and forth between parent and child tasks to support layout refreshes during React execution

  • Support render() to return multiple elements

  • Better support for Error Boundary

Since the original intention is not to allow JS to execute for long periods of time without control (manual scheduling is preferred), why does JS execution for long periods of time affect interactive responses and animations?

Because JavaScript runs on the main thread of the browser, it happens to run with style calculation, layout, and, in many cases, drawing. If the JavaScript runs for too long, it blocks this other work and can cause frames to drop.

Optimize JavaScript Execution

React hopes to change this uncontrollable situation through Fiber refactoring and further improve the interactive experience

P.S. for more information about Fiber targets, see Codebase Overview

Key features

The key features of Fiber are as follows:

  • Incremental rendering (splitting the render task into chunks and dividing it into multiple frames)

  • Ability to pause, terminate, and reuse rendering tasks when updating

  • Assign priority to different types of updates

  • New basic capabilities in concurrency

Incremental rendering is used to solve the problem of losing frames by splitting the rendering tasks into small chunks at a time, and then giving control of time back to the main thread as opposed to long chunks. This strategy is called Cooperative Scheduling, and it is one of three task scheduling strategies for operating systems (Firefox also applies this technique to the real DOM)

In addition, React’s killer feature is a virtual DOM for two reasons:

  • Coding UI is easier (don’t care what the browser should do, but describe the next moment of the UI to React)

  • If DOM can be virtual, so can other (hardware, VR, native App)

React implementation is divided into two parts:

  • Reconciler looks for the differences between the two versions of the UI before and after a moment in time. Including the former Stack Reconciler and now The Fiber Reconciler

  • Renderer is a plug-in, platform-specific part. Including React DOM, React Native, React ART, ReactHardware, ReactAframe, React-PDF, ReactThreeRenderer, ReactBlessed, and more

This wave is a makeover of the Reconciler and an enhancement of the Killer feature

Fiber and Fiber tree

The React runtime has three instances:

DOM Actual DOM node ------- Instances React Maintained vDOM tree node ------- Elements Description how the UI looks (type, props)Copy the code

Instances are created based on Elements and are abstract representations of components and DOM nodes. VDOM Tree maintains component state and the relationship between components and the DOM tree

The vDOM tree is built in the first rendering process, and when it needs to be updated later (setState()), diff vDOM tree obtains DOM change and applies the DOM change (patch) to the DOM tree

The top-down, recursive mount/update of the reconciler before Fiber, known as Stack Reconciler, cannot be interrupted (continuously occupying the mainline), so that periodic tasks such as layout, animation, and interactive responses on the mainline cannot be dealt with immediately and affect the experience

Fiber solved this problem by breaking up the render/update process (recursive diff) into a series of small tasks, checking a small part of the tree at a time to see if there is time to move on to the next task, continuing if there is, suspending yourself if there is not, and continuing when the main thread is not busy

The incremental update required more context information than the previous vDOM tree, so we extended the Fiber tree. The update process is to construct a new Fiber Tree (workInProgress Tree) based on the input data and the existing fiber tree. Therefore, the Instance layer adds these instances:

DOM real DOM node ------- effect There is an effect list on each workInProgress tree node to store diff results. Merge effect will be performed upward after the current node is updated - - - - workInProgress The workInProgress tree is a snapshot of the current progress created from fiber Tree during the Reconcile process. Used for breakpoint recovery ---- Fiber Fiber tree similar to vDOM tree, used to describe the context information required for incremental updates ------- Elements Describes what the UI looks like (type, props)Copy the code

Note: the two layers on the dotted line are temporary structures that are only useful for updates and are not maintained on a daily basis. “Effect” refers to side effect, including the DOM change to be done

The main structure of each node on the Fiber tree (each node is called fiber) is as follows:

{stateNode, child, return, sibling,... }Copy the code

Return indicates to whom the result list should be submitted after the current node finishes processing.

P.S.F iber tree is actually a Singly Linked List (Singly Linked List) tree structure, see the react/packages/react – the reconciler/SRC/ReactFiber. Js

P.S. Notice how small fiber stands for nodes on the Fiber tree and how large fiber stands for React Fiber

4. Fiber reconciler

The reconcile process is divided into two phases:

  1. (Interruptible) Render/Reconciliation constructs the workInProgress tree to get change

  2. Commit Applies these DOM changes

render/reconciliation

Based on fiber Tree, each fiber is taken as a unit of work, and the workInProgress Tree (new Fiber tree under construction) is constructed node by node from top to bottom.

The specific process is as follows (take component node as an example) :

  1. If the current node does not need to be updated, clone the child node and jump to 5. Tag it if you want to update it

  2. Update the current node state (props, state, context, etc.)

  3. ShouldComponentUpdate (), false, jump to 5

  4. Call Render () to get the new child node and create fiber for the child node (the creation process will reuse the existing fiber as much as possible, the addition and deletion of the child node will also happen here)

  5. If no child fiber is generated, the unit of work ends, merge the Effect list to return, and use the sibling of the current node as the next unit of work. Otherwise, make child the next unit of work

  6. If there is no time left, wait until the next main thread is idle before starting the next unit of work; Otherwise, start right away

  7. If there is no next unit of work (back to the root of the workInProgress Tree), phase 1 ends and the pendingCommit state is entered

It’s actually a 1-6 work cycle, with 7 being the exit, where you do one thing at a time and see if you can catch your breath. At the end of the work loop, the effect list on the root of the workInProgress Tree is all of the side effects collected (since each one is merged upward).

So, the process of building the workInProgress Tree is the process of diff, scheduling a set of tasks through the requestIdleCallback, coming back after each task to check for queue-jumping (more urgent), giving control of time back to the main thread after each set of tasks, Continue building the workInProgress Tree until the next requestIdleCallback callback

The previous P.S.Fiber Reconciler is called a Stack Reconciler because this scheduling context information is held by the system Stack. Although it was done at once before, it makes no sense to emphasize the stack, and the name is just for the convenience of differentiating Fiber Reconciler

requestIdleCallback

Notify the main thread, asking to tell me when I’m not busy that I have a few things I need to do that aren’t too urgent

Specific usage is as follows:

window.requestIdleCallback(callback[, Options]) / / sample let handle = window. RequestIdleCallback ((idleDeadline) = > {const {didTimeout, timeRemaining} = idleDeadline; Console. log(' Did you time out? ${didTimeout}`); Console. log(' timeRemaining ${timeRemaining. Call (idleDeadline)}ms'); // do some stuff const now = +new Date, timespent = 10; while (+new Date < now + timespent); Console. log(' it took ${timespent}ms to do something '); Console. log(' timeRemaining ${timeRemaining. Call (idleDeadline)}ms'); }, {timeout: 1000}); // Output result // timeout? False // The available time is 49.535000000000004ms // The available time is 38.64msCopy the code

Note that requestIdleCallback scheduling is just a hope for a smooth experience and does not absolutely guarantee anything, such as:

// do some stuff
const now = +new Date, timespent = 300;
while (+new Date < now + timespent);
Copy the code

If it takes 300ms to do things (life cycle functions in React, etc.) that aren’t controlled by React, what mechanism can guarantee smooth flow

P.S. generally has only 10-50ms remaining available time, so the scheduling space is not very rich

commit

Stage 2: Go straight to one go:

  1. Process the Effect List (including three processes: updating the DOM tree, calling component lifecycle functions, and updating internal states such as refs)

  2. End of the pair, end of phase 2, all updates are committed to the DOM tree

Note that it is really all done in one go (synchronous execution, not stop), and the actual work in this phase is quite heavy, so try not to do heavy work in the last three life cycle functions

Life cycle hook

Lifecycle functions are also divided into two phases:

/ / phase 1 render/reconciliation componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate / / Phase 2 commit componentDidMount componentDidUpdate componentWillUnmountCopy the code

Phase 1 lifecycle functions may be called multiple times, executing at low priority (one of the six priorities described below) by default, and re-executing later if interrupted by higher-priority tasks

Fiber Tree and workInProgress Tree

Double buffering, like nextListeners in Redux, is the Fiber Tree and workInProgress Tree

Double buffering: If the current pointer points to the workInProgress tree and the old Fiber tree is removed, the new fiber tree is created

The benefits of this:

  • Ability to reuse internal objects (Fiber)

  • Save time for memory allocation and GC

Each fiber has an alternate property that also points to a fiber. When creating a workInProgress node, take alternate first. If there is no alternate property, create one:

let workInProgress = current.alternate; if (workInProgress === null) { //... WorkInProgress. Alternate = current; current.alternate = workInProgress; } else { // We already have an alternate. // Reset the effect tag. workInProgress.effectTag = NoEffect; // The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null; }Copy the code

As noted in the note, Fiber and workInProgress hold references to each other. After “love the new”, the old fiber is used as the reserved space for the update of the new fiber to achieve the purpose of reuse of the fiber instance

There are also some interesting tricks in the P.S. source code, such as tag bits

Priority policy

Each unit of work runs with six priorities:

  • The Synchronous Reconciler is executed in the same manner as the previous Stack Reconciler operation

  • The task executes before the next tick

  • Animation executes before the next frame

  • High will be implemented immediately in the near future

  • Low does not matter if it is slightly delayed (100-200ms)

  • Offscreen is executed at the next render or scroll time

Synchronous first screen (first render) is used and requires as fast as possible, whether it blocks the UI thread or not. The animation is scheduled with a requestAnimationFrame so that the animation process starts immediately on the next frame; The last three are all performed by the requestIdleCallback callback; Offscreen refers to the currently hidden, off-screen (invisible) element

High priority things like keyboard input (wanting immediate feedback), low priority things like web requests, making comments appear, and so on. In addition, queue-jumping is allowed for emergencies

There are two problems with this prioritization mechanism:

  • How does the lifecycle function execute (which may be interrupted frequently) : is the sequence of firing times not guaranteed

  • Starvation (low-priority starvation) : If there are many high-priority tasks, low-priority tasks will not be executed.

Here’s an official example of the lifecycle function problem:

low A
componentWillUpdate()
---
high B
componentWillUpdate()
componentDidUpdate()
---
restart low A
componentWillUpdate()
componentDidUpdate()
Copy the code

The first issue is being addressed (not yet), the lifecycle issues will break some existing apps and make smooth upgrades difficult, and the Fiber team is working hard to find elegant upgrades

The second problem is mitigated by reusing work where it can, and it sounds like something is being done

These two problems themselves are not easy to solve, only to what extent the problem. The first problem, for example, is that if the component lifecycle functions have too many side effects, there is no way to solve it without harm. These issues will cause some resistance when it comes to upgrading Fiber, but they are by no means insoluble. (At the very least, if the new feature is attractive enough, the first problem can be solved by everyone.)

7. To summarize

known

React doesn’t work well in situations that require a high response experience, such as animations, layouts, and gestures

The root cause is that the rendering/update process, once started, cannot be interrupted and continues to occupy the main thread, which is too busy executing JS to care about anything else (layout, animation), resulting in poor experience of frame drops, delayed response (or even no response)

o

A mechanism that can completely solve the problem of the main thread being held for a long time not only deals with the immediate problem, but also has long-term significance

The “fiber” reconciler is a new effort aiming to resolve the problems inherent in the stack reconciler and fix a few long-standing issues.

solution

Break the render/update process into smaller tasks and control the time with a reasonable scheduling mechanism (finer granularity, greater control)

Then, there are five sub-problems:

1. What? What can’t be dismantled?

Divide the render/update process into 2 stages (DIff + patch) :

1.diff ~ render/reconciliation
2.patch ~ commit
Copy the code

Diff’s actual job is to compare the prevInstance and nextInstance states and find the difference and the corresponding DOM change. Diff is essentially a set of calculations (traversal, comparison) that are separable (half of them will come later)

In the patch stage, all DOM changes in this update are applied to the DOM tree, which is a series of DOM operations. These DOM operations may look like they can be split up (following the change List), but doing so can cause inconsistency between the actual state of the DOM and the internal state it maintains, as well as affecting the experience. Also, in general, the DOM update time is nothing compared to the diff and life cycle functions, so it doesn’t make a lot of sense to split, right

Therefore, the render/ Reconciliation work (DIff) can be split, while the Commit work (patch) cannot be split

P.S. Diff and reconciliation are corresponding and not equivalent. If one must distinguish, reconciliation includes diff:

This is a part of the process that React calls reconciliation which starts when you call ReactDOM.render() or setState(). By the end of the reconciliation, React knows the result DOM tree, and a renderer like react-dom or react-native applies the minimal set of changes necessary to update the DOM nodes (or the platform-specific views in case of React Native).

(From Top-down Reconciliation)

2. How to dismantle it?

Here are a few diff job splitting schemes:

  • According to the component structure. It is not easy to divide, and the workload of updating each component cannot be estimated

  • Disassemble according to actual process. For example, getNextState(), shouldUpdate(), updateState(), checkChildren() and some life cycle functions

According to the component is too thick, obviously not fair to the large component. According to the process is too fine, too many tasks, frequent scheduling is not cost-effective. So is there a good unit to split?

There is. The split unit of Fiber is Fiber (a node on the Fiber tree), which is actually disassembled according to the virtual DOM node. Because the Fiber tree is constructed based on the vDOM tree, the tree structure is identical, but the information carried by the nodes is different

So, it’s actually a split of vDOM node granularity (fiber as the unit of work), with each component instance and each DOM node abstracted as an instance of a unit of work. In the work cycle, one fiber is processed at a time, and the whole work cycle can be interrupted/suspended after processing

3. How do I schedule tasks?

Divided into 2 parts:

  • Work cycle

  • Priority mechanism

The work cycle is the basic task scheduling mechanism. In the work cycle, one task (unit of work) is processed at a time, and there is a chance for breathing after processing:

// Flush asynchronous work until the deadline runs out of time. while (nextUnitOfWork ! == null && ! shouldYield()) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }Copy the code

ShouldYield ran out to see the time is not (idleDeadline timeRemaining ()), useless over words continue to handle the next task, finished ends, return the time control to the main thread, such as the next requestIdleCallback callback then do:

// If there's work left over, schedule a new callback. if (nextFlushedExpirationTime ! == NoWork) { scheduleCallbackWithExpiration(nextFlushedExpirationTime); }Copy the code

In other words, normal scheduling (regardless of emergencies) is done by work cycles, and the basic rule is: at the end of each unit of work, check to see if there is time left for the next one, and “suspend” when there is no time left.

Prioritization mechanisms are used to handle emergencies and optimize order, for example:

  • Commit phase, increase priority

  • The high quality task is done in the middle of the error, to reduce the priority

  • Take time to focus on low-priority tasks before you starve to death

  • If the corresponding DOM node is not visible at the moment, the lowest priority is given

These policies are used to dynamically adjust task scheduling and are an auxiliary mechanism for the work cycle, doing the most important things first

4. How to interrupt/recover breakpoints?

Interrupt: Check the unit of work you are currently working on, save the current work (firstEffect, lastEffect), change the tag, close up quickly and open a requestIdleCallback for the next opportunity

Breakpoint recovery: The next time the unit of work is processed, see that the tag is the broken task and proceed with the unfinished part or redo

P.S. Whether the interrupt is “natural” when time runs out, or rudely interrupted by a higher-quality task, the interrupt mechanism is the same

5. How do I collect task results?

The working cycle for Fiber Reconciliation is as follows:

  1. Find the workInProgress tree with the highest root priority and take its pending nodes (representing components or DOM nodes)

  2. Check whether the current node needs to be updated. If not, go to 4

  3. Tag (tag), update yourself (component updates props, context, etc., DOM node notes DOM change), and generate the workInProgress node for the child

  4. If no child nodes are generated, merge the Effect List (including DOM change) to the parent

  5. Use your child or brother as a processing node to get ready for the next work cycle. If there are no pending nodes (back to the root of the workInProgress Tree), the work loop ends

The task results are collected by merging the effect list upward at the end of each node update. After reconciliation, all side effects including DOM change are recorded in the effect list of the root node

The lines

Since tasks are detachable (as long as the full Effect List is finally returned), parallel execution is allowed (multiple Fiber Reconciler + multiple workers), and the first screen is easier to load/render in chunks (vDOM Forest).

For parallel rendering, Firefox tests reportedly showed 130ms pages in 30ms, so that’s something to look forward to, and React is ready for that, which is one of the more features that we hear about in the React Fiber context

Eight. Source code analysis

From 15 to 16, the source code structure changed a lot:

  • Never see mountComponent/updateComponent (), has been split reorganization (beginWork/completeWork/commitWork ())

  • ReactDOMComponent has also been removed. In Fiber, DOM nodes are abstracted as ReactDOMFiberComponent and components are represented as ReactFiberClassComponent. Before is ReactCompositeComponent

  • The core mechanism of Fiber architecture is the ActFiberScheduler responsible for task scheduling, which is equivalent to ReactReconciler before

  • VDOM Tree has become fiber Tree. It used to be a simple top-down tree structure, but now it is a tree structure based on a single linked list, and maintains more node relationships

Fiber Tree:

fiber-tree

When you think about it for a moment, from Stack Reconciler to Fiber Reconciler, at the source level there is a recursive cycle of reconciler (which is, of course, much more than the recursive cycle, but this is the first step).

The React[15-] source code varies a lot, and it can be a bit difficult to read if you don’t know Fiber in advance.

P.S. this Tomb-sweeping flowchart is officially being retired

The resources

  • Lin Clark — A Cartoon Intro to Fiber — React Conf 2017:5 star recommendation, great sound, 100 times better than Jing Chen

  • acdlite/react-fiber-architecture

  • Codebase Overview

  • A look inside React Fiber — How work will get done