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

3 阅读2分钟

引言

在现代前端开发中,React 已经成为了最流行的 UI 库之一。然而,许多开发者只是停留在使用层面,对其底层原理知之甚少。本文将带你从零实现一个简易版的 React,重点解析 React 的核心机制——Fiber 架构和协调算法。通过这个实践过程,你不仅能深入理解 React 的工作原理,还能提升对现代前端框架设计的认识。

一、React 核心架构演进

1.1 Stack Reconciler 的局限性

在 React 16 之前,React 使用的是 Stack Reconciler(栈协调器)。这个协调器使用递归的方式遍历组件树,存在一个致命缺陷:一旦开始渲染,就必须完成整个树的遍历,无法中断。这会导致主线程被长时间占用,造成页面卡顿。

// 传统的递归渲染(简化版)
function render(element, container) {
  // 递归处理子元素
  element.children?.forEach(child => {
    render(child, container);
  });
  // 渲染当前元素
  mount(element, container);
}

1.2 Fiber 架构的革命

React 16 引入了 Fiber 架构,这是一个完全重写的协调引擎。Fiber 的核心思想是将渲染工作拆分成多个小单元,每个单元可以在浏览器空闲时执行,实现了可中断的渲染过程。

二、实现简易版 React 核心

2.1 定义 Fiber 节点结构

首先,我们需要定义 Fiber 节点的数据结构:

class FiberNode {
  constructor(type, props) {
    // 节点类型('div', 'span' 或函数组件等)
    this.type = type;
    
    // 节点属性
    this.props = props;
    
    // DOM 节点
    this.stateNode = null;
    
    // 父节点
    this.return = null;
    
    // 第一个子节点
    this.child = null;
    
    // 下一个兄弟节点
    this.sibling = null;
    
    // 备用节点(用于 diff 比较)
    this.alternate = null;
    
    // 副作用标签
    this.effectTag = null;
    
    // 第一个副作用节点
    this.firstEffect = null;
    
    // 下一个副作用节点
    this.nextEffect = null;
  }
}

2.2 实现虚拟 DOM 创建函数

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      ),
    },
  };
}

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

三、Fiber 协调算法详解

3.1 双缓存技术

React 使用双缓存技术来优化更新过程。每个 Fiber 节点都有一个 alternate 属性,指向上一次渲染的 Fiber 节点。

// 创建 workInProgress 树的根节点
function createWorkInProgress(current, pendingProps) {
  let workInProgress = current.alternate;
  
  if (workInProgress === null) {
    workInProgress = new FiberNode(current.type, pendingProps);
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.props = pendingProps;
    workInProgress.effectTag = null;
    workInProgress.firstEffect = null;
    workInProgress.nextEffect = null;
  }
  
  workInProgress.child = current.child;
  workInProgress.sibling = current.sibling;
  workInProgress.stateNode = current.stateNode;
  
  return workInProgress;
}

3.2 协调过程实现

协调过程分为两个阶段:render 阶段和 commit 阶段。

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = null;

function render(element, container) {
  wipRoot = {
    stateNode: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };
  
  deletions = [];
  nextUnitOfWork = wipRoot;
  
  // 启动工作循环
  requestIdleCallback(workLoop);
}

function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  
  requestIdleCallback(workLoop);
}

3.3 执行单元工作

function performUnitOfWork(fiber) {
  // 1. 创建 DOM 节点(如果需要)
  if (!fiber.stateNode) {
    fiber.stateNode = createDOM(fiber);
  }
  
  // 2. 协调子元素
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
  
  // 3. 返回下一个工作单元
  if (fiber.child) {
    return fiber.child;
  }
  
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.return;
  }
  
  return null;
}

function createDOM(fiber) {
  const dom =
    fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type);
  
  updateDOM(dom, {}, fiber.props);
  
  return dom;
}

3.4 子节点协调算法

这是 React 协调算法的核心部分:

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate?.child;
  let prevSibling = null;
  
  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;
    
    // 比较新旧 Fiber
    const sameType = oldFiber && element && element.type === oldFiber.type;
    
    if (sameType) {
      // 类型相同,更新节点
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        stateNode: oldFiber.stateNode,
        return: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
      };
    }
    
    if (element && !sameType) {
      // 类型不同,创建新节点
      newFiber = {
        type: element.type,
        props: element.props,
        stateNode: null,
        return: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT',
      };
    }
    
    if (oldFiber && !sameType) {
      // 删除旧节点
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }
    
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }
    
    if (newFiber) {
      if (index === 0) {
        wipFiber.child = newFiber;
      } else {
        prevSibling.sibling = newFiber;
      }
      
      prevSibling = newFiber;
    }
    
    index++;
  }
}

四、提交阶段实现

4.1 批量更新 DOM

function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

function commitWork(fiber) {
  if (!fiber) return;
  
  let domParentFiber = fiber.return;
  while (!domParentFiber.stateNode) {
    domParentFiber = domParentFiber.return;
  }
  const domParent = domParentFiber.stateNode;
  
  if (fiber.effectTag === 'PLACEMENT' && fiber.stateNode != null) {
    domParent.appendChild(fiber.stateNode);
  } else if (fiber.effectTag === 'UPDATE' && fiber.stateNode != null) {
    updateDOM(
      fiber.stateNode,
      fiber.alternate.props,
      fiber.props
    );
  } else if (fiber.effectTag === 'DELETION') {
    commitDeletion(fiber, domParent);
  }
  
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.stateNode) {
    domParent.removeChild(fiber.stateNode);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

4.2 属性更新

function updateDOM(dom, prevProps, nextProps) {
  const isEvent = key => key.startsWith('on');
  const isProperty = key => key