面试官:React 为什么需要 Fiber?

7,661 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

哈喽,大家好 我是xy👨🏻‍💻。前端面试经常会遇到面试官拷问源码方面的知识,比如:虚拟DOM,Diff算法,Fiber等... 这不前段时间就有个学弟在面试的时候就被问住了。

面试官问:

  • React Fiber 是什么?”
  • 为什么叫 “Fiber”?
  • Fiber 架构解决了什么问题?

怎么回答

遇到这样的问题,一般从以下几个方面去回答:

  • 说下 react16 之前 stack架构递归遍历组件树成本很高,会造成主线程被持续占⽤,结果就是主线程上的布局、动画等周期性任务就⽆法⽴即得到处理,造成视觉上的卡顿,影响⽤户体验
  • Fiber架构 任务分解,避免主线程的持续占用造成卡顿问题
  • 增量渲染,把渲染任务拆分成多块
  • 更新时候能够暂停终止复用渲染任务
  • 不同类型的更新赋予优先级

基本上按照上面的几个方面回答,都不会有太大的问题

那么有很多同学会问,react 中是否还有虚拟Dom,diff算法呢?

其实这里可以简单的理解为:

📚虚拟domreact 中叫Fiber, diff算法react 中叫协调

为什么会出现 React fiber 架构

React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了 16ms,用户交互就会卡顿。

Stack reconciler 的工作流程很像函数的调用过程。父组件里调子组件,可以类比为函数的递归(这也是为什么被称为stack reconciler的原因)。

setState后,react 会立即开始reconciliation过程,从父节点(Virtual DOM)开始遍历,以找出不同。将所有的Virtual DOM遍历完成后,reconciler能给出当前需要修改真实DOM的信息,并传递给renderer,进行渲染,然后屏幕上才会显示此次更新内容。

对于特别庞大的DOM树来说,reconciliation过程会很长(x00ms),在这期间,主线程是被 js 占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。

网友测试使用React V15,当DOM节点数量达到100000时, 加载页面时间竟然要 7 秒; 当然以上极端情况一般不会出现,官方为了解决这种特殊情况。在 Fiber 架构中使用了Fiber reconciler

React Fiber 是什么?

官方的一句话解释是“React Fiber是对核心算法的一次重新实现”。Fiber 架构调整很早就官宣了,但官方经过两年时间才在V16版本正式发布。官方概念解释太笼统, 其实简单来说 React Fiber 是一个新的任务调和器Reconciliation)简称协调

简单理解就是把一个耗时长的任务分解为一个个的工作单元(每个工作单元运行时间很短,不过总时间依然很长)。在执行工作单元之前,由浏览器判断是否有空余时间执行有时间就执行工作单元,执行完成后,继续判断是否还有空闲时间。没有时间就终止执行让浏览器执行其他任务(如 GUI 线程等)。等到下一帧执行时判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复到任务结束。

Fiber架构 = Fiber节点 + Fiber调度算法

要让终止的任务恢复执行,就必须知道下一工作单元对应那一个。所以要实现工作单元的连接,就要使用链表,在每个工作单元中保存下一个工作单元的指针,就能恢复任务的执行。

要知道每一帧的空闲时间,就需要使用 requestIdleCallback Api。传入回调函数,回调函数接收一个参数(剩余时间),如果有剩余时间,那么就执行工作单元,如果时间不足了,则继续requestIdleCallback,等到下一帧继续判断。

📚所以 Fiber 架构就是用 异步的方式解决旧版本 同步递归导致的性能问题

如果还不知道什么是链表的同学,可以看下我之前的文章:

为什么叫 “Fiber”?

大家应该都清楚进程(Process)和线程(Thread)的概念,进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元,在计算机科学中还有一个概念叫做 Fiber,英文含义就是“纤维”,意指比 Thread 更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。

上面说的 FiberReact Fiber 不是相同的概念,但是,React 团队把这个功能命名为 Fiber,含义也是更加紧密的处理机制,比 Thread 更细。

Fiber 架构解决了什么问题?

Reconciliation

React 官方核心算法名称是 Reconciliation , 中文翻译是“协调”!React diff 算法的实现 就与之相关。

先简单回顾下 React Diff: React 首创了“虚拟 DOM”概念, “虚拟 DOM”能火并流行起来主要原因在于该概念对前端性能优化的突破性创新

稍微了解浏览器加载页面原理的前端同学都知道网页性能问题大都出现在 DOM 节点频繁操作上

而 React 通过“虚拟 DOM” + React Diff 算法保证了前端性能

传统 Diff 算法

通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?

如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异

React Diff 算法

Virtual DOM(虚拟 Dom)树转换成 Actual DOM(真实 Dom)树的最少操作的过程,叫作协调(Reconciliaton)。diff 算法是协调的具体实现,将 O(n^3)复杂度 转化为 O(n)复杂度。

diff 算法原则:

  • 分层同级比较,不跨层比较;
  • 相同的组件生成的 DOM 结构类似;
  • 分组内的同级节点通过唯一的 id 进行区分(key)

不同类型节点比较:

  • 逐层比较,不同类型节点直接替换,组件经历 unmount,mount

同类型,不同属性节点比较:

同类型节点一般会出现以下几种形式更新:

  • 插入新的同级节点
  • 删除同级节点
  • 同级节点交换位置
  • 更新节点属性

React 中的 React fiber 架构

  1. 使用 Fiber 节点, 来代替虚拟 DOM 原来的结构。
// 链表结构
export type Fiber = {
  // Fiber 类型信息
  type: any,
  // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
  stateNode: any,
  ...
  // 指向父节点,或者render该节点的组件
  return: Fiber | null,
  // 指向第一个子节点
  child: Fiber | null,
  // 指向下一个兄弟节点
  sibling: Fiber | null,
}

  1. 通过 ReactDOM.render()setState 把待更新的任务会先放入队列中, 然后通过 requestIdleCallback 请求浏览器调度。
// 更新节点 放入数组中
updateQueue.push(updateTask);
requestIdleCallback(performWork, {timeout});
  1. 现在浏览器有空闲或者超时了就会调用 performWork 来执行任务:
// performWork 会拿到一个Deadline,表示剩余时间
function performWork(deadline) {
  // 循环取出updateQueue中的任务
  while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
    workLoop(deadline);//
  }
  // 如果在本次执行中,未能将所有任务执行完毕,那就再请求浏览器调度
  if (updateQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}
  1. 这里的 nextUnitOfWork 下一个工作单元是 Fiber 结构,所以终止了之后也能恢复继续执行
// 保存当前的处理现场
let nextUnitOfWork: Fiber | undefined // 保存下一个需要处理的工作单元
let topWork: Fiber | undefined        // 保存第一个工作单元
function workLoop(deadline: IdleDeadline) {
  // updateQueue中获取下一个或者恢复上一次中断的执行单元
  if (nextUnitOfWork == null) {
    nextUnitOfWork = topWork = getNextUnitOfWork();
  }
  // 每执行完一个执行单元,检查一次剩余时间
  // 如果被中断,下一次执行还是从 nextUnitOfWork 开始处理
  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    // 处理节点 并 返回下一个 要处理得节点
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork, topWork);
  }

  // 提交工作,当任务全部执行完后 一次全部更新 同步执行
  if (pendingCommit) {
  	// commit 阶段
    commitAllWork(pendingCommit);
  }
}

/**
 * 返回下一个 要处理的 nextUnitOfWork
 * @params fiber 当前需要处理的节点
 * @params topWork 本次更新的根节点
 */
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
  // 对该节点进行处理
  // diff算法 为修改的节点打上标签
  // 在fiber上 生成对应的stateNode (真实的DOM节点)
  beginWork(fiber);
  // 如果存在子节点,那么下一个待处理的就是子节点
  if (fiber.child) {
    return fiber.child;
  }
  // 没有子节点了,上溯查找兄弟节点
  let temp = fiber;
  while (temp) {
    completeWork(temp);// 收集副作用函数 commit 阶段执行
    // 到顶层节点了, 退出
    if (temp === topWork) {
      break
    }
    // 找到,下一个要处理的就是兄弟节点
    if (temp.sibling) {
      return temp.sibling;
    }
    // 没有, 继续上溯
    temp = temp.return;
  }
}
  1. 渲染阶段, 协调阶段完成后生成了 WorkInProgress Tree,在有修改的 Fiber 节点中都有一个标签,在 Renderer 阶段循环 WorkInProgress Tree 进行修改节点然后渲染到页面上。
// 任务都执行完后 进入commit 修改真实Tree
function commitAllWork(fiber) {
  if(!fiber) {
    return;
  }

  const parentDom = fiber.return.dom;
  if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) {
    parentDom.appendChild(fiber.dom);
  } else if(fiber.effectTag === 'DELETION') {
    parentDom.removeChild(fiber.dom);
  } else if(fiber.effectTag === 'UPDATE' && fiber.dom) {
    // 更新DOM属性
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

  // 递归操作子元素和兄弟元素
  commitRootImpl(fiber.child);
  commitRootImpl(fiber.sibling);
}

写在最后

大家好,我是一名前端🤫爱好:瞎折腾

如果你也是一名瞎折腾的前端欢迎加我微信交流哦...

🤫一定要点我