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

4 阅读1分钟

前言

在现代前端开发中,React 已经成为最流行的 UI 库之一。然而,许多开发者只是停留在使用层面,对其底层原理知之甚少。今天,我们将一起动手实现一个简易版的 React,重点解析其核心的 Fiber 架构和协调算法。通过这个实践,你不仅能深入理解 React 的工作原理,还能掌握如何构建自己的虚拟 DOM 系统。

一、React 核心概念回顾

在开始编码之前,我们先简要回顾一下 React 的几个核心概念:

  1. 虚拟 DOM:JavaScript 对象表示的 DOM 树,用于与实际 DOM 进行对比
  2. 协调(Reconciliation):React 比较新旧虚拟 DOM 并找出最小更新操作的过程
  3. Fiber 架构:React 16 引入的新架构,支持增量渲染和更好的调度能力

二、实现基础虚拟 DOM

首先,让我们定义虚拟 DOM 的数据结构:

// 虚拟 DOM 元素类型
const ELEMENT_TYPES = {
  TEXT_ELEMENT: 'TEXT_ELEMENT',
  HOST_ELEMENT: 'HOST_ELEMENT',
}

// 创建文本元素
function createTextElement(text) {
  return {
    type: ELEMENT_TYPES.TEXT_ELEMENT,
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

// 创建元素(类似 React.createElement)
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      ),
    },
  }
}

三、实现 Fiber 数据结构

Fiber 是 React 协调算法的核心数据结构,它是一个工作单元,包含了组件状态、副作用等信息:

// Fiber 节点结构
class FiberNode {
  constructor(type, props) {
    // 标识
    this.type = type
    this.key = props.key
    this.elementType = null
    
    // 链表结构
    this.return = null      // 父节点
    this.child = null       // 第一个子节点
    this.sibling = null     // 兄弟节点
    
    // 状态
    this.pendingProps = props
    this.memoizedProps = null
    this.memoizedState = null
    this.updateQueue = null
    
    // 副作用
    this.effectTag = null
    this.firstEffect = null
    this.lastEffect = null
    
    // 备用树(用于双缓存)
    this.alternate = null
    
    // 渲染相关
    this.stateNode = null   // 对应的真实 DOM 节点
  }
}

// 工作循环状态
let nextUnitOfWork = null
let wipRoot = null          // work in progress root
let currentRoot = null      // 当前渲染的根
let deletions = null        // 需要删除的节点列表

四、实现协调算法(Reconciliation)

协调算法是 React 最核心的部分,它决定了如何高效地更新 DOM:

// 协调子节点
function 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
    
    // 比较新旧 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 (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }
    
    prevSibling = newFiber
    index++
  }
}

五、实现 Fiber 工作循环

Fiber 架构的关键是增量渲染,我们将工作分解成小单元:

// 执行工作单元
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 workLoop(deadline) {
  let shouldYield = false
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
  
  requestIdleCallback(workLoop)
}

// 启动渲染
function render(element, container) {
  wipRoot = {
    stateNode: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  
  deletions = []
  nextUnitOfWork = wipRoot
  
  requestIdleCallback(workLoop)
}

六、实现提交阶段(Commit Phase)

协调阶段完成后,我们需要将变更提交到真实 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)
}

// 创建 DOM 节点
function createDOM(fiber) {
  const dom =
    fiber.type === ELEMENT_TYPES.TEXT_ELEMENT
      ? document.createTextNode('')
      : document.createElement(fiber.type)
  
  updateDOM(dom, {}, fiber.props)
  return dom
}

// 更新 DOM 属性
function updateDOM(dom, prevProps, nextProps) {
  // 移除旧的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(key => !(key in nextProps))
    .forEach(name => {
      dom[name] = ''
    })
  
  // 设置新的属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(key => prevProps[key] !== nextProps[key])
    .forEach(name => {
      dom[name] = nextProps[name]
    })
  
  // 处理事件监听器
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(key => !(key in nextProps) || prevProps[key] !== nextProps[key])
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2)
      dom.removeEventListener(eventType, prevProps[name])
    })
  
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(key => prevProps[key] !== nextProps[key])
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2)
      dom.addEventListener(eventType, nextProps[name])
    })
}

七、实现函数组件支持

让我们添加对函数组件的支持:

// 处理函数组件
function