美文网首页
React Fiber剖析

React Fiber剖析

作者: Napster99 | 来源:发表于2021-04-27 15:57 被阅读0次
    react-fiber-logo.jpeg

    React自从2013年5月开源以来,一路披襟斩棘到前端最热门框架之一,框架本身具有以下特性。

    • Declarative(声明式)
    • Component-Based(组件式)
    • Learn Once, Write Anywhere(多端渲染式)
      除此之外还有快速高效等特点,主要得益于Virtual Dom的应用,虚拟Dom是一种HTML DOM节点的抽象描述,存在JS中的结构对象中,当渲染时通过Diff算法,找到需要变更的节点进行更新,这样就节省了不必要的更新。

    React快速响应主要制约于CPU瓶颈,比如以下栗子所示:

    function App() {
    const len = 3000;
      return (
        <ul>
          {Array(len).fill(0).map((_, i) => <li>{i}</li>)}
        </ul>
    }
    const rootEl = document.querySelector("#root");
    ReactDOM.render(<App/>, rootEl); 
    

    当需要被渲染的节点很多时,有存在大量的JS计算,因为GUI渲染线程JS执行线程是互斥的,所以在JS计算的时候就会停止浏览器界面渲染行为,导致页面感觉卡顿。

    主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次,也就说渲染一帧的时间必须控制在16ms内才能保证不掉帧。

    这段时间内需要完成以下操作:

    • 脚本执行(JavaScript)
    • 样式计算(CSS Object Model)
    • 布局(Layout)
    • 重绘(Paint)
    • 合成(Composite)
      JS->Style->Layout->Paint->Composite过程
      既然JS执行比较耗时,能不能中断或暂停JS的执行,把执行权交回给渲染线程呢?
      首先看一下React是怎么去做这事的
     // react/packages/scheduler/src/forks/SchedulerHostConfig.default.js
     // Scheduler periodically yields in case there is other work on the main
      // thread, like user events. By default, it yields multiple times per frame.
      // It does not attempt to align with frame boundaries, since most tasks don't
      // need to be frame aligned; for those that do, use requestAnimationFrame.
      let yieldInterval = 5;
      let deadline = 0;
    

    从源码中可以看到,React每次会利用这部分时间(5ms)更新组件,当超过这个时间React就会将执行权就还给浏览器由浏览器自主分配执行权,React本身则等待下一帧时间来继续被中断的工作,这就引入了一个时间切片的概念。将耗时的长任务拆分到每一帧中,一次执行小块任务。总结来说就是将 同步的更新变成可中断的异步更新

    React v15 Stack Reconciler

    ReactDOM.render(<App />, rootEl);
    

    React DOM将<App />传递给Reconciler,此时Reconciler将会检查App是 函数 or

    • 【函数】 -> App(props)
    • 【类】 -> new App(props) 来实例化 App, 并调用生命周期方法 componentWillMount(),之后调用 render() 方法来获取渲染的元素

    tips: 面试过程中通常会问函数组件和类组件,两者是否都被实例化?答案就在上面

    此过程是基于树的深度遍历的递归过程(遇到自定义组件就会一直的递归下去,直到最原始的HTML标签),Stack Reconciler 的递归一旦进入调用栈就无法中断或暂停,如果当组件嵌套很深或数量极多,在16ms内无法完成就势必造成浏览器丢帧导致卡顿。
    刚在上面也提过解决方案就是将 同步的更新变成可中断的异步更新,但15版本架构不支持异步更新,所以React团队决定撸起袖子重写,折腾了两年多终于在2017/3发布了可用版本。

    React Fiber

    在首次渲染中构建出虚拟dom树,后续更新时(setState)通过diff虚拟dom树得到dom change,最后将dom change应用到真实dom树中,Stack Reconciler自顶向下递归(mount/update)无法中断导致主线程上的布局/动画/交互响应无法及时得到处理,引起卡顿。

    这些问题Fiber Reconciler 能够解决。

    Fiber原意纤维,工作最小单元,每次通过ReactDOM.render首次构建时都会生成一个FiberNode,接下来具体看下FiberNode结构。

    function FiberNode(
      tag: WorkTag,
      pendingProps: mixed,
      key: null | string,
      mode: TypeOfMode,
    ) {
      // Instance
      this.tag = tag;  // FiberNode类型,目前总有25种类型,常用的就是FunctionComponent 和 ClassComponent
      this.key = key; //和组件Element中的key一致
      this.elementType = null;
      this.type = null;  //Function|String|Symbol|Number|Object
      this.stateNode = null; //FiberRoot|DomElement|ReactComponentInstance等绑定的其他对象
    
      // Fiber
      this.return = null; // FiberNode|null 父级FiberNode
      this.child = null; // FiberNode|null 第一个子FiberNode
      this.sibling = null;// FiberNode|null 相邻的下一个兄弟节点
      this.index = 0;  //当前父fiber中的位置
    
      this.ref = null; //和组件Element中的ref一致
    
      this.pendingProps = pendingProps; // Object 新的props
      this.memoizedProps = null; // Object 处理后的新props
      this.updateQueue = null; // UpdateQueue 即将要变更的状态
      this.memoizedState = null;  //Object 处理后的新state
      this.dependencies = null;
    
      this.mode = mode; // number
      // 普通模式,同步渲染,React15-16的生产环境使用
      // 并发模式,异步渲染,React17的生产环境使用
      // 严格模式,用来检测是否存在废弃API,React16-17开发环境使用
      // 性能测试模式,用来检测哪里存在性能问题,React16-17开发环境使用
    
      // Effects
      this.flags = NoFlags;
      this.subtreeFlags = NoFlags;
      this.deletions = null; // render阶段的diff过程检测到fiber的子节点如果有需要被删除的节点
    
      this.lanes = NoLanes; //如果fiber.lanes不为空,则说明该fiber节点有更新
      this.childLanes = NoLanes; //判断当前子树是否有更新的重要依据,若有更新,则继续向下构建,否则直接复用已有的fiber树
    
      this.alternate = null; //FiberNode|null 候补节点,缓存之前的Fiber节点,与双缓存机制相关,后续讲解
    
    }
    
    

    所有fiber对象都是FiberNode实例,通过tag来标识类型。通过createFiber初始化FiberNode节点,代码如下

    const createFiber = function(
      tag: WorkTag,
      pendingProps: mixed,
      key: null | string,
      mode: TypeOfMode,
    ): Fiber {
      // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
      return new FiberNode(tag, pendingProps, key, mode);
    };
    
    

    Fiber解决这个问题的解法是把渲染/更新过程拆分成一系列小任务,每次执行一小块,再看是否有剩余时间继续下一个任务,有则继续,无则挂起,将执行线程归还。

    Fiber Tree

    通过虚拟dom树,react会再创建一个Fiber Tree,不同的Element类型对应不同类型的Fiber Node,在后续的更新过程中每次重新渲染都会重新创建Element,但是Fiber不会重新创建,只会更新自身属性。
    顾名思义,通过多个Fiber Node组成了一个Fiber Tree,也是为了满足Fiber增量更新的特性才拓展出了Fiber Tree结构。

    Fiber Tree.png
    首先每个节点是统一的,会有两个属性FirstChildNextSibiling,第一个指向节点第一个儿子节点,第二个指向下一个兄弟节点,Fiber这种单链表结构就可以把整个树串联起来。同时Fiber Tree在Instance层又新增了额外三个实例:
    • effect:每个workInProgress tree节点上都有一个effect list 存放diff结果,更新完毕后updateQueue进行收集
    • workInProgress: reconcile过程中的快照,工作过程节点,用户不可见
    • fiber:用来描述增量更新所需的上下文信息

    这里我们着重来理解一下 workInProgress到底起了什么作用?首先通过代码来看下它是如何被创建的

    export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
      let workInProgress = current.alternate;
      if (workInProgress === null) {
        workInProgress = createFiber(
          current.tag,
          pendingProps,
          current.key,
          current.mode,
        );
      // 以下两句很关键
      workInProgress.alternate = current;
      current.alternate = workInProgress;
      // do something else ...
      } else {
      // do something else ...
      }
     // do something else ...
      return workInProgress;
    }
    

    首先workInProgress一个Fiber节点,当前节点的alternate为空时,通过createFiber创建,每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换完成dom更新, 简单来说当workInProgress Tree内存中构建完成后直接替换Fiber Tree的做法,就是刚刚提到的双缓冲机制

    workInProgress-mount

    当内存中的workInProgress树直接构建完成后,直接替换了页面需要渲染的Fiber树,这是mount的过程。
    当页面其中一个node节点发生变更时,会开启一次新的render阶段并构建一颗心的workInProgress树,
    这里有个优化点就是 因为每个node节点都有一个alternate属性互相指向,在构建时会尝试复用当前current Fiber树已有的节点内属性,是否复用取决于diff算法判断。


    workInProgress-update

    在更新过程中,React在filbert tree中实际发生改变的fiber上创建effect,所有effect构成effect list链表,在commit阶段执行,实现了只对实际发生改变的fiber做dom更新,避免了遍历整个fiber tree造成性能浪费。每当一个Fiber节点的flags字段不为NoFlags时,就会把此Fiber节点添加到effect list中,根据每一个effect的effectTag类型执行对应的dom树更改。

    递归Fiber节点

    Fiber架构下的每个节点都会经历两个过程,即beginWork/completeWork。

    1、beginWork
    function beginWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
    // do something else
    }
    
    • current: 当前组件上一次更新的Fiber节点,workInProgress.alternate
    • workInProgress: 当前组件内存的Fiber节点
    • renderlanes: 相关优先级
      由于双缓存机制的存在,我们可以通过current === null 来判断组件是处于mount还是uplate,当mount时会根据fiber.tag创建不同类型的子Fiber节点,当update时 didReceiveUpdate === false就可以直接复用前一次更新的子Fiber节点,具体判断如下:
    if (current !== null) {
        const oldProps = current.memoizedProps;
        const newProps = workInProgress.pendingProps;
    
        if (
          oldProps !== newProps ||
          hasLegacyContextChanged() ||
          (__DEV__ ? workInProgress.type !== current.type : false)
        ) {
          didReceiveUpdate = true;
        } else if (!includesSomeLane(renderLanes, updateLanes)) {
          didReceiveUpdate = false;
          switch (workInProgress.tag) {
            // do something else
          }
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes,
          );
        } else {
          didReceiveUpdate = false;
        }
      } else {
        didReceiveUpdate = false;
      }
    
    2、completeWork
    function completeWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
      const newProps = workInProgress.pendingProps;
    
      switch (workInProgress.tag) {
        case IndeterminateComponent:
        case LazyComponent:
        case SimpleMemoComponent:
        case FunctionComponent:
        case ForwardRef:
        case Fragment:
        case Mode:
        case Profiler:
        case ContextConsumer:
        case MemoComponent:
          return null;
        case ClassComponent: {
         // do something else
          return null;
        }
        case HostRoot: {
          // do something else
          updateHostContainer(workInProgress);
          return null;
        }
        case HostComponent: {
          // do something else
          return null;
        }
      // do something else
    }
    

    传入参数和beginWork一致,不做过多讲解,completeWork会根据tag不同调用不同的处理逻辑。对于处理的当前节点是mount还是update阶段同样可以使用current === null 来做判断。由于completeWork属于“归”阶段的函数,每次调用appendAllChildren都会将已生成的子孙节点插入当前生成的dom节点,这样就一个完整的dom树了。

    3、effectList

    每个执行完completeWork并且存在effectTag的Fiber节点都会保存在effectList单向链表中,同时effectList第一个和最末个Fiber节点会分别保存在fiber.firstEffect /fiber.lastEffect属性中。


    effectList.png

    effectList使得commit阶段只需要遍历effectList就可以了,提高了运行性能, 至此 render阶段告一段落。

    写在最后

    我觉得React Fiber是一种解决问题的理念架构,从React16架构来说分为三层:
    Scheduler/Reconciler/Renderer
    它利用浏览器的空闲时间完成循环模拟递``归过程,所有操作都在内存中进行,只有所有组件完成Rconciler工作,才会走Renderer一次渲染展示,提升效率。
    篇幅不长,知识点零零总总,性能优化没有最好,只有更好,所以我们一直在路上...

    相关文章

      网友评论

          本文标题:React Fiber剖析

          本文链接:https://www.haomeiwen.com/subject/fetczktx.html