理解 React Firber

React 的设计理念是:React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。“快速响应“的制约因素有 CPU 和 网络:

  • CPU的瓶颈:当项目变得庞大、组件数量繁多、遇到大计算量的操作或者设备性能不足使得页面掉帧,导致卡顿
  • IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应,导致白屏。

为此 react 引入 Time Slicing 时间分片 Suspense。

React 采用 的 JSX,具有 JavaScript 的完整表现力,可以构建非常复杂的组件。但是灵活的语法,也意味着引擎难以理解,无法从模版层面进行静态分析,存在以上编译时先天不足,React 优化更多是运行时入手

React 页面渲染存在以下两个阶段:

  • 调度阶段(reconciliation):更新数据生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列
  • 渲染阶段(commit):遍历更新队列,将其所有的变更一次性更新到DOM上

React15 及以前的架构:

  • Reconciler(协调器)—— 负责找出变化的组件;采用递归的方式生成虚拟DOM,递归过程是不能中断的
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上;

Stack Reconciler,若组件树的层级很深,递归更新占用线程耗时超过16ms,浏览器引擎会从多个函数的调用的执行栈的顶端开始执行,直到执行栈被清空才会停止。然后将执行权交还给浏览器。此时浏览器得不到控制权,事件响应和页面动画因为浏览器不能及时绘制下一帧,就会出现卡顿和延迟。

从 React 16 开始至今进行重构,目的是实现 Concurrent Mode(并发模式),即实现一套可中断/恢复的更新机制,由两部分组成:

  • 一套协程架构:Fiber Reconciler;
  • 基于协程架构的启发式更新算法:控制协程架构工作方式的算法。

React17 开始支持 concurrent mode,其根本目的是为了让应用保持cpu和io的快速响应,Concurrent Mode(并发模式)的功能包括 Fiber、Scheduler、Lane,可以根据用户硬件性能和网络状况调整应用的响应速度,核心就是为了实现异步可中断的更新

首先,React16 将递归的无法中断的更新重构为异步的可中断更新——全新的Fiber架构应运而生,即 Stack Reconciler 重构为 Fiber Reconciler。

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler;
  • Reconciler(协调器)—— 负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上。

Fiber Reconciler 中用链表遍历的方式替代了 React 16 之前的栈递归

1. 使用多向链表替代树结构表示 DOM 结构

2. effect 单链表

3. state update 单链表

链表相比顺序结构(栈)的好处

  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了,插入和删除的时间复杂度是O(1)。
  2. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到它的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  2. 不能自由读取,必须找到它的上一个节点,访问的时间复杂度是O(n)。

Fiber Reconciler 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在挂起和恢复过程中起到了关键作用

然而,React16 的 expirationTimes 模型决定节点是否更新:>= expirationTimes。React17 优化的 lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。

Lane用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是需要Lane来解决。 

原则上,1s 内绘制的帧数越多,画面越细腻,而浏览器一帧一般会经过以下几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 绘制渲染
  7. 执行 RIC (RequestIdelCallback)

而最后的 RIC 事件只有在每一帧预算(60Hz 对应为16.6ms)内前面的步骤执行完留有剩余才会执行。所以 RIC 执行时间过长就会影响控制权交还给浏览器去进行下一帧的渲染,导致页面出现卡顿和事件响应不及时。

因此,以浏览器是否有剩余时间作为任务中断的标准,那么就需要当浏览器有剩余时间时的通知机制。

由于原生 requestIdleCallback API 的兼容性以及触发频率不稳定的问题,React 实现了空闲时触发回调以及供任务设置多种调度优先级的 requestIdleCallbackpolyfill——Scheduler(调度器)

Fiber

在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型(协程),帮助开发者用一种【既模块化又协作化】的方式来编排代码。协程中子程序切换不是线程切换,是由一个线程执行的由程序(或用户)自身控制切换的。把一个耗时长的任务分成运行时间很短的很多小片,唯一的线程就不会被独占,所有任务都有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 中负责任务协调的模块,若无紧急任务则继续进行更新。

React Fiber 更新过程的可控主要体现在以下方面:

  • 任务拆分:采用"化整为零"的思想,将协调阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干个只负责一个节点的处理的小任务(FiberNode)。
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 树之间的相互引用
  }
}
function performUnitWork(currentFiber){
   // beginWork(currentFiber) // 找到儿子,并通过链表的方式挂到currentFiber上,没有儿子就找后面那个兄弟
  // 有儿子就返回儿子
  if(currentFiber.child){
    return currentFiber.child;
  } 
  // 如果没有儿子,则找兄弟
  while(currentFiber){ 
    // completeUnitWork(currentFiber); // 将自己的副作用挂到父节点去
    if(currentFiber.sibling){
      return currentFiber.sibling
    }
    // 一直往上找
    currentFiber = currentFiber.return;
  }
}

component关系 对应的 fiber Node 关系:

  • 任务挂起、恢复、终止;
    • 挂起:一个小任务(FiberNode)执行完成后, requestIdleCallbackpolyfill——Scheduler(调度器)先判断当前帧预算是否有空闲,没有就挂起链表下一个小任务(FiberNode)的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务(渲染)。
    • 恢复:浏览器渲染完一帧后, requestIdleCallbackpolyfill——Scheduler(调度器)判断当前帧预算是否有剩余,若有则恢复执行之前挂起的任务。若没有小任务需要处理,代表协调阶段完成,可以开始进入渲染阶段(commit)。
    • 终止:并不是每次更新都会走到提交阶段(commit)。当在协调过程中触发了新的更新,在执行下一个任务时,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链

currentFiber 表示上次渲染构建的 Filber 树在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用与 currentFiber 对应的节点对象,减少新创建对象带来的开销。

无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。更新过程就是根据输入数据以及现有的 fiber tree (workInProgress tree)构造出新的fiber tree(workInProgress tree)

  • 任务具有优先级:在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务。过期时间的大小还代表着任务的优先级任务在执行过程中顺便收集了每个小任务 FiberNode 的副作用,通过 firstEffect、nextEffect 、lastEffect将有副作用的节点形成副作用单向链表。

其实最终都是为了收集到这条副作用链表,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的动作(commit)不能中断,不然会造成视觉上的不连贯。

未来展望

根据Fiber架构思想, isInputPending() API在 Chromium 中被实现该 API 将中断的概念用于浏览器用户交互的的功能,并且允许 JavaScript 能够检查事件队列而不会将控制权交于浏览器。它可以提高网页的响应能力,但是不会对性能造成太大影响。

目前 React 实验版本允许用户选择三种 mode:

  1. Legacy Mode: 相当于目前稳定版的模式;
  2. Blocking Mode: 应该是以后会代替 Legacy Mode 而长期存在的模式;
  3. Concurrent Mode: 以后会变成 default 的模式。

Concurrent Mode 下有两个最重要的特性在 React 18 已经很好支持了:

  1. Suspense 是 React 提供的一种异步处理的机制,它不是一个具体的数据请求库。它是React 提供的原生的组件异步调用原语。可以用来解决请求阻塞的问题。
  2. useTransition 让页面实现 Pending -> Skeleton -> Complete 的更新路径,用户在切换页面时可以停留在当前页面,让页面保持响应。 相比展示一个无用的空白页面或者加载状态,这种用户体验更加友好。

Concurrent mode(并发模式)也是未来 react 主要迭代的方向。再者,Concurrent mode 只是并发,更远的可能待解锁的能力是并行执行。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
React Native 是 Facebook 开发的一种基于 React 框架的移动应用程序开发框架。它允许开发人员使用 JavaScript 和 React 的知识来构建原生移动应用程序。React Native 的核心思想是“一次编写,多处运行”,即使用相同的代码库可以在多个平台上运行,包括 iOS 和 Android 等。 理解 React Native 需要掌握以下几个方面: 1. React Native 应用程序的架构:React Native 应用程序的架构基于组件化和数据驱动的思想。开发人员可以使用组件来构建应用程序界面,这些组件可以通过状态管理库来管理和更新应用程序的数据。 2. React Native 应用程序的开发工具:React Native 提供了一系列开发工具,包括调试工具、模拟器、热重载等,可以帮助开发人员更快地构建和调试应用程序。 3. React Native 应用程序的原生组件:React Native 包含一些原生组件,如文本、图像、按钮等,这些组件可以帮助开发人员构建原生的应用程序界面。 4. React Native 应用程序的扩展性:React Native 允许开发人员编写原生模块和插件,这些模块和插件可以扩展应用程序的功能,如访问设备硬件、使用第三方库等。 总之,理解 React Native 需要掌握其应用程序的架构、开发工具、原生组件和扩展性等方面。通过不断地学习和实践,开发人员可以更好地利用 React Native 开发高质量的移动应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值