react 为什么需要 fiber

560 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情

核心描述

  • 为什么需要 fiber,以及其解决了什么问题:
    • React 15 及以前的架构中, Reconciler (协调器)采用递归的方式创建虚拟 DOM,递归过程中是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了 16ms,用户交互就会卡顿。
    • 为了解决这个问题,React 16 将递归的无法中断更新重构为异步的可中断更新,由于曾经用于递归的虚拟 DOM 数据结构已经无法满足需要。于是产生了 React Fiber 架构。
  • fiber 本质是什么
    • 在广义的计算机科学概念中,Fiber 是一种协作的变成模型(协程),帮助开发者用一种既模块化又协作化的方式来编排代码。
    • 在 React 中,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前采用递归需要一气呵成影响性能的做法。
  • 在 React 中 Fiber 包含的三层含义:
    • 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
    • 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
    • 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

知识拓展

  • React 15-17 架构演变的简要概述:

    • React 15 架构可以分为两层:
      • Reconciler(协调器):负责找出变化的组件,递归实现,不可中断
      • Renderer(渲染器):负责将变化的组件渲染到页面上
    • React 16 架构可以分为三层:
      • Scheduler(调度器):调度任务的优先级,高优先级的任务先进入 Reconciler
      • Reconciler(协调器):负责找出变化的组件,采用 Fiber 架构实现,将递归变成了可以中断的循环
      • Renderer(渲染器):负责将变化的组件渲染到页面上(此过程不可以中断)
    • React 17 架构优化:
      • 在 React 16 的基础上,加入了 Concurrent 模式,目的是实现一套可中断/恢复的更新机制,需要注意在稳定版本默认没有使用此模式,需要手动设置渲染模式为 Concurrent 才可以
      • 将 React 16 的 expirationTimes 模型优化为 Lanes 模型
  • React 和 Vue 的本质区别:

    • Vue 是静态分析 template 文件,采用预编译优化,在解析模板的同时构建 AST 依赖树,同时标记出可能会变化的动态节点。利用数据双向绑定,进行数据拦截或代理,进行响应式处理。从而能够比较精准的计算出有改变的 DOM,减少计算量。
    • React 是局部渲重新渲染,核心就是一堆递归的 React.createElement 的执行调用。其优化的方向是不断的优化 React.createElement 的执行速度,让其更快,更合理的创建最终的元素。
  • 关于 Fiber 其他的一些知识点:

    • Fiber 节点的数据结构示例:
    class FiberNode {
          constructor(tag, pendingProps, key, mode) {
            // 实例属性
            this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
            this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的
            this.elementType = null; // createElement的第一个参数,ReactElement 上的 type
            this.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
            this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
            // fiber
            this.return = null; // 父节点,指向上一个 fiber
            this.child = null; // 子节点,指向自身下面的第一个 fiber
            this.sibling = null; // 兄弟组件, 指向一个兄弟节点
            this.index = 0; //  一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
            this.ref = null; // reactElement 上的 ref 属性
            this.pendingProps = pendingProps; // 新的 props
            this.memoizedProps = null; // 旧的 props
            this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新
            this.memoizedState = null; // 对应  memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
            this.mode = mode; // 表示当前组件下的子组件的渲染方式
            // effects
            this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新(更新、删除等)
            this.nextEffect = null; // 指向下个需要更新的fiber
            this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个
            this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
            this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
            this.childExpirationTime = NoWork; // child 过期时间
            this.alternate = null; // current 树和 workInprogress 树之间的相互引用
          }
        }
    
    • Fiber 节点构成的 Fiber 树就对应 DOM 树
    • 双缓存技术:
      • 在 React 中最多存在两个 Fiber 树,页面上渲染的是 current Fiber 树,内存中构建的是 workInProgress Fiber 树
      • 两个树的关联,是通过每个树中 Fiber 节点的 alternate 属性连接
    • React 的 diff 算法就是基于 Fiber 节点的状态来实现的
    • React 的 Scheduler 调度算法,也是基于 Fiber 数据结构来实现
    • 最终的 render 是通过将 ReactFiberHostConfig 和 ReactDOMHostConfig 关联起来,生成页面中的实际 DOM

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。