最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Fiber 原理剖析

    正文概述 掘金(涂鸦大前端)   2021-05-10   732

    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 计算的时候就会停止浏览器界面渲染行为,导致页面感觉卡顿。

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

    • 脚本执行(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 是 函数

    • 【函数】 -> App(props)
    • 【类】 -> new App(props) 来实例化 App, 并调用生命周期方法

    componentWillMount(),之后调用 render() 方法来获取渲染的元素

    此过程是基于树的深度遍历的递归过程(遇到自定义组件就会一直的递归下去,直到最原始的 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.rende 首次构建时都会生成一个 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 结构。

    React Fiber 原理剖析

    首先每个节点是统一的,会有两个属性 FirstChildNextSibiling,第一个指向节点第一个儿子节点,第二个指向下一个兄弟节点,Fiber 这种单链表结构就可以把整个树串联起来。同时 Fiber Tree 在 Instance 层又新增了额外三个实例:

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

    // This is used to create an alternate fiber to do work on.
    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 的做法,就是刚刚提到的 双缓冲机制

    React Fiber 原理剖析

    当内存中的 workInProgress 树直接构建完成后,直接替换了页面需要渲染的 Fiber 树,这是 mount 的过程。

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

    React Fiber 原理剖析

    在更新过程中,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 属性中。

    React Fiber 原理剖析

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

    写在最后

    我觉得 React Fiber是一种解决问题的理念架构,从 React16 架构来说分为三层:Scheduler/Reconciler/Renderer

    它利用浏览器的空闲时间完成循环模拟 过程,所有操作都在内存中进行,只有所有组件完成 Rconciler 工作,才会走 Renderer 一次渲染展示,提升效率。


    起源地下载网 » React Fiber 原理剖析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元