React fiber 架构浅析

1,006 阅读4分钟

为什么会出现 React fiber架构

React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。 在这里插入图片描述 React16 Fiber Reconciler 通过把diff算法分成很多小片。当一个小片执行完成时,由浏览器判断是否有时间继续执行新任务,没时间就终止执行,有时间就检查任务列表中有没有新的、优先级更高的任务,有就做这个新任务,一直重复这个操作。 在这里插入图片描述

什么是 React fiber架构

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

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

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

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

React 中的 React fiber架构

数据结构 使用 Fiber节点, 来代替虚拟DOM原来的结构。

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

在这里插入图片描述 简介协调阶段 通过ReactDOM.render()setState 把待更新的任务会先放入队列中, 然后通过 requestIdleCallback 请求浏览器调度。

// 更新节点 放入数组中
updateQueue.push(updateTask);
requestIdleCallback(performWork, {timeout});

现在浏览器有空闲或者超时了就会调用performWork来执行任务:

// performWork 会拿到一个Deadline,表示剩余时间
function performWork(deadline) {
  // 循环取出updateQueue中的任务
  while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
    workLoop(deadline);// 
  }
  // 如果在本次执行中,未能将所有任务执行完毕,那就再请求浏览器调度
  if (updateQueue.length > 0) {
    requestIdleCallback(performWork);
  }
}

这里的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;
  }
}

diff算法 对比步骤

在这里插入图片描述 简介渲染阶段 协调阶段完成后生成了 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);
}

参考文章

React 技术揭秘
这可能是最通俗的 React Fiber(时间分片) 打开方式
Deep In React 之浅谈 React Fiber 架构(一)