(译)Fiber 架构之于 React 的意义

原文地址javascript

背景知识

Fiber 架构主要有两个阶段:reconciliation / render(协调/渲染) 和  commit(提交阶段)。在源代码中  reconciliation 阶段一般被划归为 render阶段。 这个阶段 React遍历组件树执行一下工做:html

  • 更新 stateprops
  • 调用生命周期钩子
  • 父级组件中经过调用 render 方法,获取子组件 (类组件),函数组件则直接调用获取
  • 将获得的子组件与以前已经渲染的组件比较,也就是 diff
  • 计算出须要被更新的 DOM

上述全部活动都涉及到 Fiber 的内部工做机制。具体工做的区分执行则基于 React Element 的类型。例如,对一个类组件 Class Component 来讲,React须要去实例化这个类(也就是 new Component(props)),然而对一个函数组件  Function Component 来讲则没有必要。 若是感兴趣,在这里能够看到 Fiber中定义的全部工做目标类型。这些活动正是安德鲁在演讲中所提到了的。java

When dealing with UIs, the problem is that if too much work is executed all at once, it can cause animations to drop frames… 当处理UIs时,问题是若是大量的工做一次性执行,那么就可能致使动画掉帧……node

那么,关于 ‘all at once’ 的部分指的是什么呢?基本上能够这样认为,若是 React 同步遍历整个组件树,那么它须要为每一个组件执行对应的数据渲染更新等工做。因而当组件树较大时就会形成这部分代码的执行时间超过 16ms ,进而致使浏览器画质渲染掉帧,肉眼可见的卡顿感。react

那怎么办呢?git

Newer browsers (and React Native) implement APIs that help address this exact problem… 相对较新的浏览器都实现了一个新的API ,它能够帮助解决这个问题……github

他提到的这个新的 API 是一个名字叫  requestIdleCallback 的全局方法,它能够在浏览器空闲时间段内调用函数队列。能够这样使用它:web

requestIdleCallback((deadline)=>{
    console.log(deadline.timeRemaining(), deadline.didTimeout)
});
复制代码

若是你如今打开浏览器并还行上面的代码,Chrome  的日志将会打印  49.885000000000005 false 。基本上能够认为是浏览器告诉你,你如今有 49.885000000000005 ms 能够作任何你须要去作事情,而且时间尚未用完。反之deadline.didTimeout 则是 true 。请记住, 一旦浏览器开始一些工做后,timeRemaining 就随时被改变 。算法

requestIdleCallback is actually a bit too restrictive and is not executed often enough to implement smooth UI rendering, so React team had to implement their own version._ 实时上 requestIdleCallback 的限制太多,不能被屡次连续的执行来实现流畅的UI渲染,因此 React 团队被迫实现一个本身的版本。数组

如今若是咱们将 React 在一个组件上须要执行的全部活动都集中在  performWork函数中,那么若是使用   requestIdleCallback 处理调度工做的话,咱们的代码应该是这样的:

requestIdleCallback((deadline) => {
    // while we have time, perform work for a part of the components tree
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
        nextComponent = performWork(nextComponent);
    }
});
复制代码

咱们在一个组件上执行相关工做,完成后会返回下一个将要被处理组件的引用。这也就是安德鲁以前讨论过的问题:

in order to use those APIs, you need a way to break rendering work into incremental units 为了使用这些APIs,你须要一种方式将渲染工做分割为一个个的增量单元。

为了解决这个问题,React 被迫从新实现了一个新的算法,从基于内部栈调用的同步递归模型转变为基于链表指针异步模型。关于这部分安德鲁曾写过:

If you rely only on the [built-in] call stack, it will keep doing work until the stack is empty…Wouldn’t it be great if we could interrupt the call stack at will and manipulate stack frames manually? That’s the purpose of React Fiber. Fiber is re-implementation of the stack, specialized for React components You can think of a single fiber as a virtual stack frame. 若是你依赖于内部调用栈,那么它将一直工做直到栈被清空……,若是咱们能够暂停调用栈,而且去改变它,这样岂不是很棒。这其实就是 Fiber 的最终目的。Fiber 是一个从新实现的栈,主要针对 React 组件。你甚至能够吧单个 fiber 看作是一个栈的虚拟帧。

这将是我接下来要解释的。

关于栈的一些知识

我猜大家对栈概念应该不陌生。若是在代码中打上断点,在浏览器中运行,这时大家将看到它。这里有一段来自维基百科的相关描述:

In computer science, a call stack is a stack data structure that stores information about the active subroutines of a computer program… the main reason for having call stack is to keep track of the point to which each active subroutine should return control when it finishes executing… A call stack is composed of stack frames… Each stack frame corresponds to a call to a subroutine which has not yet terminated with a return. For example, if a subroutine named DrawLine is currently running, having been called by a subroutine DrawSquare, the top part of the call stack might be laid out like in the adjacent picture. 在计算机科学中,一个调用栈其实就是一个存储着计算机程序中的活动子程序相关信息的数据结构……,主要缘由是调用栈能够追踪活动子程序在完成执行后返回的控制位置……,一个调用栈由多个栈数据帧组成……,每一个数据帧对应一个尚未执行结束的子程序的调用。例如,一个叫 DrawLine 的子程序正在运行,它是以前被子程序DrawSquare 所调用,因而这个栈顶的结构相似于下面这张图片。

_

stack.png


栈 和 React 到底什么关系

像咱们文章以前提到了,React 在遍历整个组件树期间须要为组件执行相关更新对比等操做。React 以前所实现的协调器使用的是基于浏览器内部栈同步递归模型。这里有官方提供的文档对这部分的描述以及递归相关的讨论:

默认状况下,当递归一个 DOM 节点的孩子节点的时候,React 将会同时遍历两个孩子列表,当有任何不一样的时候就会生成新的改变后的孩子节点。

仔细想一想,每一次的递归调用都会添加一个数据帧到栈中。并且这整个过程都是同步的。假如咱们有下面的组件树:

componentTree.png

使用对象来代替 render 函数。你甚至能够认为它们是组件树的实例。

const a1 = {name: 'a1'};
const b1 = {name: 'b1'};
const b2 = {name: 'b2'};
const b3 = {name: 'b3'};
const c1 = {name: 'c1'};
const c2 = {name: 'c2'};
const d1 = {name: 'd1'};
const d2 = {name: 'd2'};

a1.render = () => [b1, b2, b3];
b1.render = () => [];
b2.render = () => [c1];
b3.render = () => [c2];
c1.render = () => [d1, d2];
c2.render = () => [];
d1.render = () => [];
d2.render = () => [];
复制代码

React 须要迭代这个组件树,而且为每一个组件执行一些工做。为了简单起见,这部分工做被设计在日志中打印当前组件的名称和检索它的孩子组件。

递归遍历

迭代这个这棵树的主函数叫 walk , 下面是它的实现:

walk(a1);

function walk(instance) {
    doWork(instance);
    const children = instance.render();
    children.forEach(walk);
}

function doWork(o) {
    console.log(o.name);
}
复制代码

获得的输出结果:

a1, b1, b2, c1, d1, d2, b3, c2
复制代码

递归方法直觉上是很适合遍历整个组件树。可是咱们发现它其实有必定的局限性。最重要的一点是它不能将要遍历的组件树分割为一个个的增量单元来处理,也就是说不能暂停遍历工做在某个特殊的组件上,然后继续。React 会遍历整个组件树直到栈被清空为止。

so,那么 React 在不使用递归的状况下如何实现遍历算法呢?实际上它使用了一种单链表遍历算法,它让遍历暂停成为一种可能。

链表遍历

为了实现这个算法,咱们须要一种数据结构,它包含一下 3 个  fields

  • child —— 第一个孩子节点的引用
  • sibling —— 第一个兄弟节点的引用
  • return —— 父节点的引用

React 中新的协调算法的上下文就是一种被称为 Fiber 的数据结构,它必须包含上面提到的 3 个 fields。它的底层实现是经过 React Element 提供的数据来建立一个一个的 Fiber 节点。

下面的图表展现了 fiebr node 链接起来的层次结构,经过链表和之间的属性相互链接:

fiber.png


如今咱们开始定义本身的 Node 结构。

class Node {
    constructor(instance) {
        this.instance = instance;
        this.child = null;
        this.sibling = null;
        this.return = null;
    }
}
复制代码

下面这个函数是拿到一个 nodes 数组,而后将他们链接起来。咱们要用它去链接被 render 方法返回的节点。

function link(parent, elements) {
    if (elements === null) elements = [];

    parent.child = elements.reduceRight((previous, current) => {
        const node = new Node(current);
        node.return = parent;
        node.sibling = previous;
        return node;
    }, null);

    return parent.child;
}
复制代码

这个函数会从数组的最后一个元素开始迭代并将他们链接起来造成一个单链表。它会返回列表中第一个节点的引用。下面的 demo 用来演示它是如何工做的:

const children = [{name: 'b1'}, {name: 'b2'}];
const parent = new Node({name: 'a1'});
const child = link(parent, children);

// the following two statements are true
console.log(child.instance.name === 'b1');
console.log(child.sibling.instance === children[1]);
复制代码


这里咱们还要实现一个工具函数,它可协助节点执行一些工做。在咱们的例子中,它将在日志中打印输出组件的名字。除此以外它还会取出该组件的孩子组件并将它们 链接起来:

function doWork(node) {
    console.log(node.instance.name);
    const children = node.instance.render();
    return link(node, children);
}
复制代码

ok,如今咱们能够实现核心遍历算法了。它本质上是一个深度优先算法:

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
        // 为当前的 node 执行一些工做,并将它与其孩子节点链接起来,并返回第一个孩子节点引用
        let child = doWork(current);

        // 若是孩子节点存在,则将它设置为接下来 被 doWork 方法处理的 node
        if (child) {
            current = child;
            continue;
        }

        // 若是已经返回到更节点说明遍历完成,则直接退出
        if (current === root) {
            return;
        }

        // 当前节点的兄弟节点不存在的时候,返回到父节点,继续寻找父节点的兄弟节点,以此类推
        while (!current.sibling) {

            // 若是已经返回到更节点说明遍历完成,则直接退出
            if (!current.return || current.return === root) {
                return;
            }

            // current 指向父节点
            current = current.return;
        }
        
		// current 指向兄弟节点
        current = current.sibling;
    }
}
复制代码

若是咱们查看上面的算法的实如今调用栈中的状况,相似于下图:

callstack.gif

想必你也看到了,遍历整个组件树的时候我栈并无叠加。可是若是咱们在  doWork 函数中 加上 debugger  的时候,咱们会看到下面的过程:

stacklog.gif

它几乎和浏览起得调用栈相同。因此使用咱们本身实现的这个算法能够有效的代替浏览器实现的调用栈。这其实就是安德鲁所描述的:

对 React 组件来讲 ,Fiber 是栈的从新实现。你甚至能够认为单个 Fiber 就是一个虚拟栈 frame。

咱们经过保留 node 的引用(栈顶),来控制整个栈。

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
            ...

            current = child;
            ...
            
            current = current.return;
            ...

            current = current.sibling;
    }
}
复制代码

咱们能够再任什么时候候通知遍历操做,也能够在随后从新启动。这就是咱们想要的,接下来就可以使用新的 requestIdleCallback API 来实现调度工做了。

React 中的工做循环

这里能够看到 React 中循环的具体实现:

function workLoop(isYieldy) {
    if (!isYieldy) {
        // Flush work without yielding
        while (nextUnitOfWork !== null) {
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
    } else {
        // Flush asynchronous work until the deadline runs out of time.
        while (nextUnitOfWork !== null && !shouldYield()) {
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
    }
}
复制代码

你能够看到,它的实现很好的对应了上面讲到的算法。它老是将 current Fiber Node 的引用保存到 nextUnitOfWork 变量中,用来扮演栈顶。

一般在出现交互式 UI 更新的状况下, 如(click,input,等等)时,该算法会同步遍历整个组件树,为每一个 Fiber Node 执行相关工做。固然它也能够异步遍历,正在执行 Fiber Node 完成后,会检查是否还有时间剩余。shouldYield 函数基于 deadlineDidExpire 和 deadline 这两个变量计算并返回 true or false ,用于决定是否暂停遍历。这两个变量在 React 遍历执行工做过程当中也是不断被更新改变的。

总结

译者添加

Fiber 架构对于 React 简单的来讲能够理解为让 React 摆脱了浏览器栈的约束,能够根据浏览器的空闲状态选择是否执行渲染工做,就总体渲染时间来讲并无缩短,反而是拉长了,整个渲染工做被划分为多个过程,这些过程都分散在浏览器的各个空闲时间段内,就 UI 而言,对用户来讲视觉上会更加流畅。

相关文章
相关标签/搜索