fiber了解

5 阅读4分钟

核心原理:可中断的增量更新

Fiber 架构的核心目标是将同步的、不可中断的递归渲染,改为异步的、可中断的增量更新。这依赖于两个基础:

  1. 链表数据结构:将树形结构转换为链表,使遍历可以暂停和恢复。
  2. 循环模拟递归:用循环代替递归,在每次循环后检查是否有更高优先级任务需要处理。

浏览器一帧内都做了什么事理想情况下,一帧应在16.6ms内完成(1000ms/60fps)

  1. 事件处理
  2. requestAnimationFrame回调(在每帧开始前执行,这是浏览器推荐的动画执行时机)
  3. JavaScript执行
  4. 样式计算
  5. 布局
  6. 绘制
  7. 合成
  8. 提交到GPU
  9. requestIdleCallback,浏览器一帧内任务完成后还有空闲时间执行

伪代码

以下是实现上述流程的最核心的伪代码。

1. 调度器:控制渲染任务循环

调度器是大脑,负责在浏览器空闲时执行任务,并在时间片用完时中断。

// 下一个要处理的工作单元(Fiber节点)
let nextUnitOfWork = null;

// 主循环,由浏览器的 requestIdleCallback 驱动
function workLoop(deadline) {
  // 当还有任务,且当前帧还有剩余空闲时间,就继续工作
  while (nextUnitOfWork && deadline.timeRemaining() > 1) {
    // performUnitOfWork 处理当前节点,并返回下一个节点
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  // 如果任务没做完,但时间用完了,就中断本次渲染,下次空闲时继续
  if (nextUnitOfWork) {
    requestIdleCallback(workLoop);
  } else {
    // 如果所有节点都处理完了,就进入不可中断的提交阶段,更新DOM
    commitRoot();
  }
}

// 启动第一次调度
requestIdleCallback(workLoop);

2. 工作单元:如何处理单个Fiber节点

这是渲染阶段的核心,负责对单个Fiber节点进行深度优先遍历。

// 处理单个Fiber节点,并返回下一个待处理的节点
function performUnitOfWork(fiber) {
  // 0. 在这个函数中进行了diff
  // 1. 开始工作:处理当前节点(如执行函数组件、打上副作用标记等)
  beginWork(fiber);

  // 2. 优先返回子节点,进行深度优先遍历
  if (fiber.child) {
    return fiber.child;
  }

  // 3. 如果没有子节点,则尝试返回兄弟节点或回溯到父节点
  let currentFiber = fiber;
  while (currentFiber) {
    // 完成当前节点的工作(如创建DOM实例、收集副作用)
    completeWork(currentFiber);

    // 先找兄弟节点
    if (currentFiber.sibling) {
      return currentFiber.sibling;
    }
    // 没有兄弟节点,则回溯到父节点
    currentFiber = currentFiber.return;
  }
  // 遍历完成,返回null
  return null;
}

特点

  • 可中断:将整个渲染过程分解为以 Fiber节点为单位的任务。
  • 增量渲染:通过 workLoop循环,在浏览器空闲时间片内分批执行这些任务。
  • 恢复能力:利用Fiber节点的 child, sibling, return指针构成的链表结构,记住遍历进度,从而可以从中断处继续。

旧架构了解

React 15 及之前版本使用的栈调和(Stack Reconciler)

核心原理:同步递归,不可中断

其最根本的特征是:更新过程是深度优先的同步递归,一旦开始,就无法中断。这会导致如果组件树层次很深或计算复杂,JavaScript 线程会长时间占用主线程,造成页面卡顿或无响应。

伪代码

1. 协调器(Reconciler):递归进行 Diff

这是最核心的部分,负责递归遍历整个组件树,找出差异。

// 协调器:递归更新整棵树(不可中断)
function reconcile(component) {
  // 1. 处理当前组件:生成新虚拟DOM,并与旧的进行Diff
  const oldVNode = component._currentVNode;
  const newVNode = component.render(); // 调用render方法
  
  // 简化的Diff逻辑
  if (oldVNode.type !== newVNode.type) {
    // 类型不同,直接替换整个组件
    replaceComponent(component, newVNode);
  } else {
    // 类型相同,更新属性,然后递归比较子节点
    updateProps(component, newVNode.props);
    reconcileChildren(component, newVNode.children);
  }
  
  // 更新当前虚拟DOM的引用
  component._currentVNode = newVNode;
}

// 递归比较子节点
function reconcileChildren(parentComponent, newChildren) {
  const oldChildren = parentComponent._currentVNode.children;
  
  // 遍历子节点数组,逐个递归调用reconcile
  for (let i = 0; i < newChildren.length; i++) {
    const oldChild = oldChildren[i];
    const newChild = newChildren[i];
    
    // 递归调用!一旦开始,无法停止。
    reconcile(getComponentForVNode(oldChild, newChild));
  }
}

2. 渲染器(Renderer):同步更新 DOM

协调器完成某个节点的 Diff 后,会立即通知渲染器更新真实 DOM,两者是交替工作的。

// 渲染器:将差异同步应用到真实DOM
function replaceComponent(oldComponent, newVNode) {
  // 1. 卸载旧组件
  oldComponent._domNode.remove();
  // 2. 创建并挂载新组件
  const newDomNode = createDomFromVNode(newVNode);
  oldComponent._parentNode.appendChild(newDomNode);
}