从零实现一个简易版 React:深入理解 Fiber 架构与协调算法

3 阅读2分钟

前言

在现代前端开发中,React 无疑是最受欢迎的框架之一。然而,许多开发者只是停留在使用层面,对其底层原理知之甚少。今天,我们将一起动手实现一个简易版的 React,重点探索其核心的 Fiber 架构和协调算法。通过这个实践,你不仅能深入理解 React 的工作原理,还能提升解决复杂前端架构问题的能力。

一、为什么需要 Fiber 架构?

在 React 16 之前,React 使用的是 Stack Reconciler(栈协调器)。这个协调器有一个致命缺陷:一旦开始渲染,就无法中断。如果组件树很大,JavaScript 会长时间占用主线程,导致用户交互卡顿。

Fiber 架构的诞生正是为了解决这个问题。它引入了以下关键特性:

  1. 可中断的渲染过程:将渲染工作拆分成多个小任务
  2. 增量渲染:可以分段完成渲染工作
  3. 优先级调度:不同更新拥有不同优先级
  4. 更好的错误处理:引入了 Error Boundaries

二、Fiber 节点的数据结构

让我们首先定义 Fiber 节点的基本结构:

class FiberNode {
  constructor(type, props) {
    // 节点类型
    this.type = type; // 'div', 'span', 或函数组件、类组件
    this.props = props;
    this.key = props.key;
    
    // 节点关系
    this.return = null; // 父节点
    this.child = null; // 第一个子节点
    this.sibling = null; // 下一个兄弟节点
    
    // 渲染相关
    this.stateNode = null; // 对应的真实 DOM 节点
    this.alternate = null; // 对应的旧 Fiber 节点
    
    // 副作用标记
    this.effectTag = null; // PLACEMENT, UPDATE, DELETION
    this.firstEffect = null; // 第一个有副作用的子节点
    this.lastEffect = null; // 最后一个有副作用的子节点
    this.nextEffect = null; // 下一个有副作用的节点
    
    // 状态
    this.pendingProps = props;
    this.memoizedProps = null;
    this.memoizedState = null;
    
    // 对于函数组件
    this.memoizedState = null; // hooks 链表
    
    // 工作单元状态
    this.expirationTime = 0; // 过期时间
  }
}

三、实现简易协调算法

协调算法(Reconciliation)是 React 的核心,它负责比较新旧虚拟 DOM 树的差异。让我们实现一个简化版本:

class MiniReact {
  constructor() {
    this.wipRoot = null; // work in progress root
    this.currentRoot = null; // 当前渲染的根节点
    this.nextUnitOfWork = null; // 下一个工作单元
    this.deletions = []; // 需要删除的节点
  }

  // 创建虚拟 DOM 元素
  createElement(type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children.map(child =>
          typeof child === 'object'
            ? child
            : this.createTextElement(child)
        ),
      },
    };
  }

  createTextElement(text) {
    return {
      type: 'TEXT_ELEMENT',
      props: {
        nodeValue: text,
        children: [],
      },
    };
  }

  // 渲染入口
  render(element, container) {
    this.wipRoot = {
      dom: container,
      props: {
        children: [element],
      },
      alternate: this.currentRoot,
    };
    
    this.deletions = [];
    this.nextUnitOfWork = this.wipRoot;
    
    // 启动工作循环
    requestIdleCallback(this.workLoop.bind(this));
  }

  // 工作循环
  workLoop(deadline) {
    let shouldYield = false;
    
    while (this.nextUnitOfWork && !shouldYield) {
      this.nextUnitOfWork = this.performUnitOfWork(
        this.nextUnitOfWork
      );
      shouldYield = deadline.timeRemaining() < 1;
    }

    if (!this.nextUnitOfWork && this.wipRoot) {
      this.commitRoot();
    }

    requestIdleCallback(this.workLoop.bind(this));
  }

  // 执行工作单元
  performUnitOfWork(fiber) {
    const isFunctionComponent = 
      fiber.type instanceof Function;
    
    if (isFunctionComponent) {
      this.updateFunctionComponent(fiber);
    } else {
      this.updateHostComponent(fiber);
    }

    // 返回下一个工作单元
    if (fiber.child) {
      return fiber.child;
    }
    
    let nextFiber = fiber;
    while (nextFiber) {
      if (nextFiber.sibling) {
        return nextFiber.sibling;
      }
      nextFiber = nextFiber.return;
    }
    
    return null;
  }

  // 处理函数组件
  updateFunctionComponent(fiber) {
    // 设置 hooks 索引
    this.wipFiber = fiber;
    this.hookIndex = 0;
    this.wipFiber.hooks = [];
    
    // 执行函数组件
    const children = [fiber.type(fiber.props)];
    this.reconcileChildren(fiber, children);
  }

  // 处理宿主组件(DOM 元素)
  updateHostComponent(fiber) {
    if (!fiber.dom) {
      fiber.dom = this.createDom(fiber);
    }
    
    this.reconcileChildren(fiber, fiber.props.children);
  }

  // 协调子节点
  reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;

    while (index < elements.length || oldFiber != null) {
      const element = elements[index];
      let newFiber = null;

      // 比较新旧节点
      const sameType =
        oldFiber &&
        element &&
        element.type === oldFiber.type;

      if (sameType) {
        // 类型相同,更新节点
        newFiber = {
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom,
          return: wipFiber,
          alternate: oldFiber,
          effectTag: 'UPDATE',
        };
      }
      
      if (element && !sameType) {
        // 类型不同,创建新节点
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null,
          return: wipFiber,
          alternate: null,
          effectTag: 'PLACEMENT',
        };
      }
      
      if (oldFiber && !sameType) {
        // 删除旧节点
        oldFiber.effectTag = 'DELETION';
        this.deletions.push(oldFiber);
      }

      if (oldFiber) {
        oldFiber = oldFiber.sibling;
      }

      if (newFiber) {
        if (index === 0) {
          wipFiber.child = newFiber;
        } else {
          prevSibling.sibling = newFiber;
        }

        prevSibling = newFiber;
      }

      index++;
    }
  }

  // 提交变更到 DOM
  commitRoot() {
    this.deletions.forEach(this.commitWork.bind(this));
    this.commitWork(this.wipRoot.child);
    this.currentRoot = this.wipRoot;
    this.wipRoot = null;
  }

  commitWork(fiber) {
    if (!fiber) return;

    let domParentFiber = fiber.return;
    while (!domParentFiber.dom) {
      domParentFiber = domParentFiber.return;
    }
    const domParent = domParentFiber.dom;

    if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
      domParent.appendChild(fiber.dom);
    } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
      this.updateDom(
        fiber.dom,
        fiber.alternate.props,
        fiber.props
      );
    } else if (fiber.effectTag === 'DELETION') {
      this.commitDeletion(fiber, domParent);
    }

    this.commitWork(fiber.child);
    this.commitWork(fiber.sibling);
  }

  // 创建 DOM 节点
  createDom(fiber) {
    const dom =
      fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode('')
        : document.createElement(fiber.type);

    this.updateDom(dom, {}, fiber.props);
    return dom;
  }

  // 更新 DOM 属性
  updateDom(dom, prevProps, nextProps) {
    // 移除旧的属性
    Object.keys(prevProps)
      .filter(isEvent)
      .filter(
        key =>
          !(key in nextProps) ||
          isNew(prevProps, nextProps)(key)
      )
      .forEach(name => {
        const eventType = name
          .toLowerCase