preface

This article is mainly to collect some official or other platform articles for translation, may intersperse some personal understanding, if there are mistakes or omissions, also hope criticism and correction. I haven’t studied the source code yet, but I hope this article will be part of inspire you to discuss React Fiber together in the future.

Note: In most cases, the following first person does not represent the translator, but the author of the corresponding article, please pay attention to the distinction.

React basic

Basic theoretical concepts

This article is my attempt to formally introduce some conceptual models of React itself. The aim is to describe, deductively, the sources of inspiration for such designs.

Of course, some of the assumptions here are controversial, and the actual design may have bugs or omissions. But it’s also a good start for us to talk about this formally. In the meantime, if you have a better idea, welcome pr too. Let’s go along this line and think about a series of problems from simple to complex. Don’t worry, there’s not much framework detail here.

The actual React implementation is full of pragmatism, incremental, algorithmic optimization, old and new code interchangeover, various debugging tools, and just about anything you can think of to make it more useful. Of course, these things, like version iterations, are short-lived, and if they’re useful enough, we’ll keep updating them. Again, the actual implementation is very, very complicated.

conversion

The core premise of React is that the UI is just a mapping of data -> data. The same input means the same output. Very simple pure function.



function NameBox(name) {
  return { fontWeight: 'bold', labelContent: name };
}Copy the code


'Sebastian Markbåge'- > {fontWeight: 'bold'.labelContent: 'Sebastian Markbåge' };Copy the code

abstract

However, not all UIs can do this, because some UIs are very complex. Therefore, it is important that the UI can be abstracted into many, many reusable pieces without exposing the internal implementation details of those pieces. It’s like calling a function within a function.



function FancyUserBox(user) {
  return {
    borderStyle: '1px solid blue',
    childContent: [
      'Name: ',
      NameBox(user.firstName + ' ' + user.lastName)
    ]
  };
}Copy the code


{ firstName: 'Sebastian', lastName: 'Markbåge' } ->
{
  borderStyle: '1px solid blue',
  childContent: [
    'Name: ',
    { fontWeight: 'bold', labelContent: 'Sebastian Markbåge' }};Copy the code

combination

To achieve this reusability feature, it is not enough to simply reuse leaf nodes and create a new container for them each time. We also need to build abstractions at the container level and combine other abstractions. In my opinion, composition is the transformation of two or more abstractions into a new one.



function FancyBox(children) {
  return {
    borderStyle: '1px solid blue',
    children: children
  };
}

function UserBox(user) {
  return FancyBox([
    'Name: ',
    NameBox(user.firstName + ' ' + user.lastName)
  ]);
}Copy the code

state

A UI is not just a simple service or logical state in a business. In fact, for a particular projection, many states are concrete, but for other projections, this may not be the case. For example, if you are typing in a text box, these characters can be copied to another TAB or mobile device (although you don’t want to copy them, just to distinguish them from the next example). However, data such as the position of the scrollbar is something you almost never want to copy across multiple projections (because on this device the scrollbar is 200, but scrolling up to 200 on other devices is usually not the same).

We tend to make our data models immutable. At the top, we string together all the functions that update the state, treating them like an atom (which might be easier to understand as a transaction).



function FancyNameBox(user, likes, onClick) {
  return FancyBox([
    'Name: ', NameBox(user.firstName + ' ' + user.lastName),
    'Likes: ', LikeBox(likes),
    LikeButton(onClick)
  ]);
}

// Implementation Details

var likes = 0;
function addOneMoreLike(a) {
  likes++;
  rerender();
}

// Init

FancyNameBox(
  { firstName: 'Sebastian', lastName: 'Markbåge' },
  likes,
  addOneMoreLike
);Copy the code

Note: This example updates the status through side effects. My conceptual model for this reality is to return to the state of the next phase during each update. Of course, it might seem easier not to do this, but we’ll eventually choose to change the way this example is used later (because of the side effects).

The cache

We know that for pure functions, calling the same thing over and over again is a huge waste of time and space. We can build cached versions of these functions, tracking the inputs and outputs of the last call. Next time, you can return the result directly without having to evaluate it again.



function memoize(fn) {
  var cachedArg;
  var cachedResult;
  return function(arg) {
    if (cachedArg === arg) {
      return cachedResult;
    }
    cachedArg = arg;
    cachedResult = fn(arg);
    return cachedResult;
  };
}

var MemoizedNameBox = memoize(NameBox);

function NameAndAgeBox(user, currentTime) {
  return FancyBox([
    'Name: ',
    MemoizedNameBox(user.firstName + ' ' + user.lastName),
    'Age in milliseconds: ',
    currentTime - user.dateOfBirth
  ]);
}Copy the code

List/collection

Most UIs are made up of lists that produce different values for each element in the list (such as data.map(item => < item… / >)). This creates a natural hierarchy.

To manage the state of each list element, we can create a Map to manage each specific list element.



function UserList(users, likesPerUser, updateUserLikes) {
  return users.map(user => FancyNameBox(
    user,
    likesPerUser.get(user.id),
    () => updateUserLikes(user.id, likesPerUser.get(user.id) + 1))); }var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
  likesPerUser.set(id, likeCount);
  rerender();
}

UserList(data.users, likesPerUser, updateUserLikes);Copy the code

Note: Now we have multiple different inputs passed to FancyNameBox. That would break the caching strategy we mentioned in the previous section, because we can only remember one value at a time. (Because the above memoize function takes only one parameter)

renewed

Unfortunately, there are so many lists nested within each other in the UI that we have to manage them explicitly with a lot of template code.

We can move some of the template code out of our main logic by delaying execution. For example, by using currying (which can be implemented by bind) (of course we know that bind does not fully implement currying). Then we can get rid of the template dependency by passing the state outside the core function.

This doesn’t reduce the template code, but at least it moves it out of the core logic.



function FancyUserList(users) {
  return FancyBox(
    UserList.bind(null, users)
  );
}

const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
constresolvedBox = { ... box, children: resolvedChildren };Copy the code

Here, of course



function FancyUserList(users) {
  return FancyBox(
    UserList(users, likesPerUser, updateUserLikes)
  );
}Copy the code

But it’s a bit of a hassle to expand, and to add and delete we need to change the code in the FancyUserList. Most importantly, if we want to replace likesPerUser and updateUserLikes with other collections and functions, we must create another function, such as:



function FancyUserList2(users) {
  return FancyBox(
    UserList(users, likesPerUser2, updateUserLikes2)
  );
}Copy the code

Of course, you might think that you could just set FancyUserList to accept multiple parameters. The problem with this is that every time you need to use the FancyUserList, you need to take all the parameters. Const foo = fancyUserlist. bind(null, data.users), foo(bar1, func1), foo(bar2, func2). It also implements the variable and unchangeable parts of the separation program that we talk about in design patterns. But such an implementation leaves the bind operation in the hands of the caller, which could be improved, as mentioned in the example.

State map

We learned long ago that once we see the same parts, we can use composition to avoid implementing the same parts over and over again. We can move that extracted logic around and pass it to lower-level or lower-level functions, functions that we reuse a lot.



function FancyBoxWithState( children, stateMap, updateState ) {
  return FancyBox(
    children.map(child => child.continuation(
      stateMap.get(child.key),
      updateState
    ))
  );
}

function UserList(users) {
  return users.map(user => {
    continuation: FancyNameBox.bind(null, user),
    key: user.id
  });
}

function FancyUserList(users) {
  return FancyBoxWithState.bind(null,
    UserList(users)
  );
}

const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);Copy the code

Cache map

It is difficult to cache multiple elements in a cache list. You have to figure out some cache algorithms that do a good job of balancing caching and frequency, but these algorithms are very complex.

Fortunately, the UI in the same area is usually stable and does not change.

Here we can still use the same caching state trick we just did, passing memoizationCache by composition



function memoize(fn) {
  return function(arg, memoizationCache) {
    if (memoizationCache.arg === arg) {
      return memoizationCache.result;
    }
    const result = fn(arg);
    memoizationCache.arg = arg;
    memoizationCache.result = result;
    return result;
  };
}

function FancyBoxWithState( children, stateMap, updateState, memoizationCache ) {
  return FancyBox(
    children.map(child => child.continuation(
      stateMap.get(child.key),
      updateState,
      memoizationCache.get(child.key)
    ))
  );
}

const MemoizedFancyNameBox = memoize(FancyNameBox);Copy the code

Philosophy of algebra

You’ll notice that it’s a bit like PITA, adding the things you need (values/parameters) bit by bit through several different levels of abstraction. Sometimes this also provides a quick way to pass data between two abstractions without the help of a third party. In React, we call this context.

Sometimes the dependencies between data are not as neat and consistent as an abstract tree. For example, in a layout algorithm, you need to know the size of the rectangular region of each child node before you can fully determine the position of all the byte points.

Now, this example is a bit “out there”. I’ll use Algebraic Effects as proposed for ECMAScript. If you’re familiar with functional programming, they’re avoiding the intermediate ceremony imposed by monads.

The above paragraph is not translated so as not to mislead



function ThemeBorderColorRequest(a) {}function FancyBox(children) {
  const color = raise new ThemeBorderColorRequest();
  return {
    borderWidth: '1px',
    borderColor: color,
    children: children
  };
}

function BlueTheme(children) {
  return try {
    children();
  } catch effect ThemeBorderColorRequest -> [, continuation] {
    continuation('blue'); }}function App(data) {
  return BlueTheme(
    FancyUserList.bind(null, data.users)
  );
}Copy the code

React Fiber architecture

The React Stack VS Fiber video is posted here instead of reading more. Since it’s on YouTube, I recorded a GIF (a bit big, 18M, please wait for it to download) for your convenience.

Introduction to the

React Fiber is an ongoing rewrite of the React core algorithm. It’s a culmination of the React team’s work over the past two years.

React Fiber aims to improve animation, layout, and gesture friendliness. Its most important feature is called “incremental/progressive” rendering: that is, the rendering work is broken up into smaller chunks and propagated between frames.

Other key features include: 1. The ability to pause, pause, and resume work when an update comes. 2. Different capabilities assign different priorities to different types of updates. 3. New concurrency primitives.

About this Document

Fiber introduces several new concepts that are hard to understand just by looking at the code. This document was originally a collection of notes I took while working on the React project when I was working on Fiber’s implementation. As my notes grew, I realized that this could be a useful resource for others as well. Acdlite, the author of this document, is a member of the Facebook team and not part of the React framework team. Sebmarkbage was the leader of the React team and the author of the old and new core algorithms.)

I will try to describe it as simply as possible and avoid unnecessary jargon. Links to resources are also provided when necessary.

Please note that I’m not part of the React team, nor do I have enough authority. So this is not an official document. I have invited members of the React team to review the accuracy of this document.

Fiber is a work in progress and will likely be reworked until it’s finished. So is this document, which is likely to change over time. Any suggestions are welcome.

My goal is that after reading this document, when Fiber is done, you’ll understand it better as it’s implemented. And even eventually React.

To prepare

Before continuing, I strongly recommend that you make sure that you are familiar with the following:

React Components, Elements, and Instances – “Components” is generally a very broad term. It is vital to have a firm grasp of these terms.

Reconciliation – A high-level overview of the React coordination/scheduling algorithm.

React Basic Concepts – This is an abstract description of some of the conceptual models in React. It doesn’t matter. You’ll see.

React Design Principles – Please note the Scheduling section, which explains React Fiber very well.

review

If you haven’t already, reread the “Preparation” section above. Before we explore, let’s look at a few concepts.

What is reconciliation?

Reconciliation: An algorithm that React uses to distinguish between two trees to determine which part needs to change.

Update: A change in the data causes a rendering, usually the result of setState, which eventually triggers a re-rendering.

The core idea of the React API is to think/decide/schedule how to update as if it would cause the entire app to re-render. It allows developers to think declaratively without worrying about how to efficiently transition an app from one state to another (A to B, B to C, C to A, etc.).

In fact, rerendering the entire app with each change only works on very small apps. In a real world app, this is too expensive in terms of performance. React has been optimized in this way to create a re-rendered version of the app without sacrificing performance. Most optimizations are part of the reconciliation process.

Reconciliation is an algorithm that lies behind what is commonly known as the “virtual DOM”. To summarize, when you render a React app, you create a tree of nodes that describe the app and store it in memory. The tree is then refreshed and translated into a specific environment. For example, in the browser environment, it is translated into a series of DOM operations. When the app is updated (usually via setState), a new tree is created. The new tree diff with the previous tree, and then figure out what needs to be done to update the entire app.

While Fiber is a complete rewrite of the Reconciler, the general description of the core algorithm in the React documentation still applies. The key points are:

  • Different component types are assumed to produce essentially different types of trees. React doesn’t try to diff them, instead replacing old trees entirely.

  • Diff of a list is done with a key. Keys should be stable, predictable, and unique.

Reconciliation vs rendering

DOM is just one of the things React can render, but there are also Native controls for IOS and Android that are generated through React Native. (This is why “virtual DOM” is a misnomer.)

React supports so many rendering targets because of the design of React itself. Reconciliation and rendering are two separate stages. The Reconciler does the job of calculating which parts of the tree are changing, and the renderer does the job of updating our applications with the results produced by the Reconciler. (The process of calculating tree differences is universal.)

This separation means that React DOM and React Native can both share the same coordinator logic provided by React and use their respective renderers for rendering.

Fiber rewrote the coordinator. It doesn’t care about rendering, although the renderer needs to make some changes accordingly (and take advantage of) something about this new algorithm.

scheduling

Scheduling is the process of deciding when a task should be done.

Work: Any computation that needs to be performed is a task. Tasks are usually caused by an update. (e.g., setState)

The React Design Principles document explains this very well, so HERE’s a quick quote:

In the current version of the implementation, React iterates recursively through the tree to update and calls the render function in a working cycle. However, in the future it may delay some updates to avoid losing frames.

Frame is a concept introduced in Fiber due to the use of requestAnimationFrame. The Fiber stack is used to coordinate the operation of frames. (The Fiber stack is also a concept in Fiber and is a simulation of the function call stack.) . Deferred update is relative to recursive traversal, that is, to temporarily interrupt the recursion and go through another node. Check out the video of the talk, or take a look at this GIF (a bit big, 20 MB) and the image that divides the frames

This is a common topic in React design. Some frameworks implement a “push” approach, performing calculations as new data becomes available. React, however, insists on a “pull” approach, deferring calculations until necessary.

React is not a general-purpose data processing framework. It is a framework for building user interfaces. We think it has its own unique position in an application to know which relevant computations are currently needed and which are not currently needed.

If something is not visible (off-screen), we can delay executing any logic associated with that part. If data is arriving faster than frame refreshes, we can merge and batch those updates. We can prioritize tasks from the user interface (for example, an animation triggered by clicking a button) over less priority tasks (such as rendering data retrieved from the network) to avoid frame loss.

The key points are:

  • In the UI, not every update needs to be shown to the user immediately. In fact, doing so would be wasteful, cause frame loss and degrade the user experience.

  • Different types of updates have different priorities – animation transitions need to be faster than updating data.

The full priority can be defined in the source code

  • The push based approach requires the app (programmer) to decide how to schedule these tasks. A pull-based approach makes the React framework smart to help us make these choices.

React currently doesn’t make very good use of scheduling, and an update will cause the entire setup to be re-rendered. Improving React’s core algorithm to make better use of scheduling is the idea behind Fiber.

Now we are ready to dive into Fiber’s implementation. The next section is going to be a little bit more technical than what we’ve discussed so far. Make sure you have a basic understanding of what you have read before you continue.

What is the Fiber

We’ll discuss React Fiber’s core architecture. Fiber is a much lower level of abstraction than application developers are generally aware of. If you find it hard to understand, don’t be discouraged. Keep trying, and you’ll see through the clouds. (When you finally understand its understanding, please suggest to me how to improve this section.)

Let’s get started

Our established goal with Fiber is to enable React with scheduling capabilities. Specifically, we need to be able to:

  • Pause and resume tasks.

  • Give different tasks different priorities.

  • Reuse previously completed tasks.

  • Abort tasks that are no longer needed.

To do any of these, we first need a way to break work/tasks down into lots and lots of units. In a sense, that’s Fiber. A fiber represents the unit of the task.

To further understand, let’s go back to the earlier concept of using the React component as a function of data, usually expressed as:

V = f (d)

Thus, rendering a React application is similar to calling another function in a function class. This analogy is useful when thinking about Fiber.

Typically, the way a computer keeps track of a program’s execution/invocation is through the Call stack. When a function is executed, a new stack frame is pushed onto the stack. That stack frame represents the task being performed in that function. (This may sound a little clunky, but anyone who has watched the Call Stack while debugging in any language should know.)

The problem when dealing with the UI is that too many tasks at once can result in lost frames and stuttering animations. More importantly, some of those tasks might not be necessary if a new update deprecates some of them. This is where there is a difference between UI components and function decomposition, because components usually have more specific concerns than functions.

Newer browsers (and React Native) implement apis that help solve these specific problems :requestIdleCallback lets a low-priority function be called during the idle period. RequestAnimationFrame causes a higher-priority function to be called in the next animation frame. The problem is that in order to use these apis, you need to break the rendering into incremental units. If you rely only on the call stack, it will work until the call stack is empty.

Wouldn’t it be better to optimize the rendering UI if we could customize the behavior of the call stack? Wouldn’t it be better if we could interrupt the call stack arbitrarily and manipulate the stack frame manually?

That’s the goal of React Fiber. Fiber is a stack rewrite, especially for the React component. You can think of a single fiber as a virtual stack frame.

The advantage of rewriting the stack is that you can keep stack frames in memory (this link is interesting and worth looking at) and execute them in any way at any time. This is crucial for us to complete the dispatch.

Handling stack frames manually, in addition to scheduling, may allow us to have some potential features, such as concurrency and error boundary handling. We will discuss these in a later section.

The structure of the Fiber

Emsp Note: As we focus more specifically on implementation details, we may discover more possibilities. If you find any errors or information that is too old, please refer us to pr.

In concrete terms, a fiber is a JS object that contains a component and its inputs and outputs.

A fiber corresponds to a stack frame, but it also corresponds to an instance of a component.

Here is a list of some of the most important attributes that belong to Fiber (note that they are not completely listed) :

The type and the key

The Fiber type attribute and key attribute provide the same functionality for the React element. (In fact, when a fiber is created from an element, both properties are copied over.)

A fiber type describes the component to which it corresponds. In the case of a function or class component, type is the function or class component itself. For the host component (div, span, and so on), type is a string (“div”, “span”). This is exactly the same as React, as you will notice if you use React devTools to debug React.

Conceptually, type is a function (like v = f(d)) whose execution is tracked by the stack frame.

The key, along with the Type, is used in the reconciliation process to determine whether the fiber can be reused. (unique identifier for this child)

The child and (

These two properties refer to other fibers and describe the recursion tree structure of one fiber. (Source code description: one-way linked list tree structure)

The Fiber corresponding to the Child attribute corresponds to the return value of a component’s Render method. So, in the following example:



  function Parent(a) {
    return <Child />
  }Copy the code

The child attribute of the Parent corresponds to the child.

The Sibling attribute explains cases where multiple child nodes are returned in the Render method (a new feature in Fiber). It can also return a string. I believe that we are looking forward to a long time, no longer need to set a div. Another big feature is error boundaries.)



  function Parent(a) {
    return [<Child1 />.<Child2 />]}Copy the code

The sub-fiber forms a single linked list whose head node is the first element in the array. So in the example above, the Parent’s child attribute is Child1, and the sibling attribute of Child1 is Child2.

Returning to our analogy with functions, you can think of a subfiber as a tail-calling function.

return

The value of the return property is also a fiber, pointing to the value returned after processing the current fiber. Similar in concept to the return address of a stack frame.

If a fiber has multiple subfibers, the return property for each subfiber executes the parent fiber. So in our example in the previous section, the return property of Child1 and Child2 both had the value Parent.

PendingProps and memoizedProps

Conceptually, props is the arguments of a function. A Fiber pendingProps was set when it was originally called. MemoizedProps is set at the end of the execution. (Cache a pure function)

When the upcoming pendingProps and memoizedProps are equal, this signals that fiber’s previous output can be reused so that unnecessary tasks can be avoided.

pendingWorkPriority

The value pendingWorkPriority represents the priority of the task. ReactPriorityLevel lists the different priorities and what they represent.

The NoWork priority value is 0. A higher priority number indicates a lower priority (0 is the highest priority). For example, you can use the following function to check whether a fiber has at least a specified priority.



  function matchesPriority(fiber, priority) {
    returnfiber.pendingWorkPriority ! = =0 &&
           fiber.pendingWorkPriority <= priority
  }Copy the code

This function is for illustrative use only and is not really part of the React Fiber codebase.

The scheduler uses the Priority attribute to search for the next task unit to execute. We will discuss this algorithm in the section fuTrue.

alternate

Flush: To flush a fiber is to render its output onto the screen.

Work-in-progress: represents an unfinished fiber, conceptually similar to a stack frame that has not yet returned.

At any time, an instance of a component can have up to two fibers associated with it: the current refreshed fiber and the working in-progress fiber.

The alternate of the current Fiber is the running fiber, and the alternate of the running fiber is the current fiber. (Source: Gamasutra)

Instead of always creating a new object, a fiber back-up is created using a function called cloneFiber. CloneFiber tries to reuse a fiber spare if one exists to minimize memory allocation.

Although you should think of the alternate property as an implementation detail, you’ll see it frequently in source code, so it’s worth mentioning here.

output

Host Component: Represents the leaf node of a React application. These are different in different rendering environments (for example, in browser applications, they are div, span, etc.). In JSX, they are represented by lowercase names. (Full classification is available)

Conceptually, a fiber output is the return value of a function.

Each fiber ends up with an output, but the output is created only in the leaf node of the host environment. The output is then translated/migrated to the actual DOM tree.

The output is what is ultimately passed to the renderer so that it can refresh in the render environment to reflect those changes. How the output is created and updated is the responsibility of the renderer.

Future possibilities

That’s all we’ve talked about so far. But this document is far from complete. In the future I may describe some of the algorithms that are frequently used in the life cycle of updates. They include:

  • How does the scheduler know which cell to execute next?

  • How are priorities tracked and propagated in fiber trees?

  • How does the scheduler know when to pause and resume a task?

  • How are tasks refreshed and marked as completed?

  • How do side effects (such as life cycle functions) work?

  • What is a coroutine? How is it used to implement features like context and layout?

More recommended

React-Future

Fiber Principles: Contributing To Fiber

React 15.5 and 16 Umbrella

Fiber Simplify coroutines by making yields stateless

Fiber Umbrella for remaining features / bugs

React Perf Scenarios

Fiber Compute the Host Diff During Reconciliation

fiber-debugger

Why, What, and How of React Fiber with Dan Abramov and Andrew Clark

Pete Hunt: The Past, Present and Future of React

Dan Codes

In addition to collecting some stuff Dan posted on Twitter, you can go to the link and CTRL + F search for Fiber.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — the 2017-4-16 day update — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — ———

That @reactiflux Q&A from @acdLite. See the discord discussion for more on this

Acdlite was not part of the React team at the time of writing this article, but later joined the React team. Refer to the description in this tweet. In addition, it also mentioned that I wrote the article from the perspective of a bystander at that time. After participating in the development of Fiber in the React project team, many things in the article also need to be updated. It will be updated later, and the translation will also be updated if I remember.


Did this article help you? Welcome to join the front End learning Group wechat group: