React Fiber架构的原理是什么?为什么需要Fiber?

3,425 阅读4分钟

React concurrent mode 简介想了的请点击这里

fiber架构

fiber架构是为了支持react进行可中断渲染,降低卡顿,提升流畅度。

react16之前的版本,diff虚拟dom时候是一口气完成的。这可能造成卡顿,因为人眼可识别的帧率是1s 60帧,即16ms一帧,如果diff时间超过16ms,阻塞渲染,就会感觉卡顿。

为了避免这种情况,需要让diff操作不超过16ms,如果超过16ms,就先暂停,让给浏览器进行渲染操作,后续渲染间隙再继续diff。

fiber架构就是为了支持这种“可中断渲染”而涉及的。fiber tree是一种数据结构,它把虚拟dom tree连接成一个链表,从而可以让遍历操作可以支持断点重启。

fiber tree

diff的工作就是遍历虚拟dom树,因此让diff工作能够支持断点重启,就是让遍历操作能够支持断点重启。

为此,React设计了fiber tree数据结构,每个fiber tree的node都有3个属性:return(指向父节点)、sibling(指向右兄弟节点)、child(指向第一个子节点)。

如何通过这几个属性遍历呢?

首先我们看之前的递归遍历方式。

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

    while (true) {
        // perform work for a node, retrieve & link the children
        let child = doWork(current);

        // if there's a child, set it as the current active node
        if (child) {
            current = child;
            continue;
        }

        // if we've returned to the top, exit the function
        if (current === root) {
            return;
        }

        // keep going up until we find the sibling
        while (!current.sibling) {

            // if we've returned to the top, exit the function
            if (!current.return || current.return === root) {
                return;
            }

            // set the parent as the current active node
            current = current.return;
        }

        // if found, set the sibling as the current active node
        current = current.sibling;
    }
}

// 访问节点,并生成child node
function doWork(node) {
    console.log(node.instance.name);
    const children = node.instance.render();
    return link(node, children);
}

// 创建fiber节点、并初始化return、child、sibling属性,并返回子节点
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;
}

通过上面的分析可以看出,fiber tree并非通过严格的链表来进行遍历,它也是一个树的结构,它的遍历过程和深度优先遍历一个树没有区别,区别在于加了几个属性指向相关节点,让遍历可以暂停和重启,很方便地找到一个节点的DFS下一个节点。

fiber可以理解是一种数据结构,是一个树的结构,fiber节点记录的是操作,包括将要进行的操作和已经完成的操作。而fiber架构是包含数据结构和调度机制的一个整体。

diff过程

使用fiber架构进行diff工作和之前有所不同。

在render函数中创建的React Element树在第一次渲染的时候会创建一颗结构一模一样的Fiber节点树。不同的React Element类型对应不同的Fiber节点类型。一个React Element的工作就由它对应的Fiber节点来负责。

一个React Element可以对应不止一个Fiber,因为Fiber在update的时候,会从原来的Fiber(我们称为current)clone出一个新的Fiber(我们称为alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。

上面提到时间分片的计算方法,React会记录diff操作时间,如果大于一帧的渲染时间则暂停,然后等待下个渲染间隙再继续执行diff操作,直到diff完成。这个机制由调度器来完成。

基于requestIdleCallback实现的。关于该API可以参考另一篇文章。(实际上React为了照顾绝大多数的浏览器,自己实现了requestIdleCallback。)

Fiber的基本规则:更新任务分成两个阶段,Reconciliation Phase和Commit Phase。Reconciliation Phase的任务干的事情是,找出要做的更新工作(Diff Fiber Tree),就是一个计算阶段,计算结果可以被缓存,也就可以被打断;Commmit Phase 需要提交所有更新并渲染,为了防止页面抖动,被设置为不能被打断。


注意,这种改动带来的问题是,有些生命周期钩子可能被执行多次,因此使用时候需要保证这些生命周期钩子中执行的方法多次调用不会影响逻辑。

React团队提供了替换的生命周期方法。建议如果使用以上方法,尽量用纯函数。

简单地说,diff过程是

  1. 首次渲染时候构建一个和虚拟dom树一样结构的fiber树
  2. 组件更新时候,遍历新旧fiber树,diff区别,diff操作是分片进行,16ms内如果没完成,就先暂停等待下个渲染空闲时间再继续。
  3. diff完成之后进行commit,将变化提交,进行对应的dom操作,为防止界面抖动,commit是一次性完成的。