【手写React源码】用 TypeScript 实现一个简化版 React

用 TypeScript 实现一个简化版 React


目标

通过我们的32节课的源码学习,实现一个最小可运行的 React 子集,包括以下核心功能:

功能 描述
JSX 解析 将 JSX 转换为虚拟 DOM(VDOM)
Virtual DOM 构建 表示组件结构和属性
Fiber 构建与渲染 使用 Fiber 架构进行递归构建和渲染
更新队列与调度机制 支持异步调度任务
Hook 支持 实现基本的 useStateuseEffect

注意: 这是一个教学性质的简化版本,不用于生产环境。


文件结构设计

项目结构注释详解

my-react/
├── index.html          #  应用入口HTML文件(挂载React应用的根容器)
├── index.tsx           #  TypeScript入口文件(启动React应用的核心逻辑)
├── react/              #  React核心实现目录(自定义迷你React框架源码)
│   ├── jsx.ts          #  JSX编译器(将JSX语法转换为虚拟DOM)
│   ├── vdom.ts         #  虚拟DOM管理(创建/更新/对比虚拟DOM树)
│   ├── fiber.ts        # ⚡ Fiber架构核心(实现协调算法与调度单元)
│   ├── reconciler.ts   #  协调器(Diff算法实现与DOM操作指令生成)
│   ├── hook.ts         #  自定义Hooks系统(函数组件状态管理)
│   └── scheduler.ts    #  调度器(实现优先级调度与时间分片)
└── components/        #  组件目录(存放业务组件)
    └── App.tsx        #  根组件(应用入口组件,包含核心业务逻辑)

项目架构图(ASCII版)

┌──────────────────────┐
│    index.html        │   容器层
└──────────────────────┘
           │
           ▼
┌──────────────────────┐
│    index.tsx         │   入口层
│ (挂载点 + 初始渲染)  │
└──────────────────────┘
           │
           ▼
┌──────────────────────────────┐
│          react/              │   核心引擎层
├──────────────────────────────┤
│ ┌──────────┐ ┌──────────┐   │
│ │  jsx.ts  │ │  vdom.ts │   │   虚拟DOM层
│ └──────────┘ └──────────┘   │
│           │                  │
│           ▼                  │
│     ┌─────────────┐          │
│     │  fiber.ts    │          │  ⚡ Fiber架构
│     │(双缓冲/协调) │          │
│     └─────────────┘          │
│           │                  │
│           ▼                  │
│     ┌─────────────┐          │
│     │ reconciler.ts│          │   Diff算法
│     └─────────────┘          │
│           │                  │
│           ▼                  │
│     ┌─────────────┐          │
│     │  hook.ts     │          │   Hooks系统
│     └─────────────┘          │
│           │                  │
│           ▼                  │
│     ┌─────────────┐          │
│     │ scheduler.ts │          │   调度系统
│     └─────────────┘          │
└──────────────────────────────┘
           │
           ▼
┌──────────────────────┐
│   components/App.tsx │   业务组件层
└──────────────────────┘

核心架构说明

  1. 入口层 (index.tsx)

    • 初始化React根节点(ReactDOM.render等价实现)
    • 创建初始Fiber根节点(wipRoot
    • 启动协调过程(nextUnitOfWork调度)
  2. 虚拟DOM层

    • jsx.ts: 实现JSX到createElement的转换
    • vdom.ts:
      • 虚拟DOM树构建
      • 类型标记(TEXT_ELEMENT特殊处理)
      • 属性标准化(自动处理children
  3. Fiber架构

    • 链表结构:child/sibling/return指针实现树遍历
    • 双缓冲技术:alternate指针实现旧/新树对比
    • 副作用标记:effectTag记录DOM操作指令
  4. 协调器 (reconciler.ts)

    • Diff算法实现:
      • 元素类型对比
      • Key优化(列表重组)
      • 移动/插入/删除操作标记
    • 生成更新队列(effectList
  5. Hooks系统

    • 链表管理:HookState通过next指针串联
    • 状态持久化:通过alternate实现跨Fiber版本状态同步
    • 调度时机:与Fiber节点生命周期绑定
  6. 调度器

    • 优先级队列:按任务类型(同步/异步)调度
    • 时间分片:通过requestIdleCallback实现
    • 协调控制:nextUnitOfWork指针驱动调度流程

架构特点

  1. 分层解耦

    • 渲染层(组件)与引擎层(Fiber/调度)完全解耦
    • 通过type字段实现函数组件/类组件的统一处理
  2. 增量渲染

    • 通过Fiber链表实现可中断的协调过程
    • 调度器控制渲染粒度(时间分片)
  3. 状态管理

    • Hooks系统与Fiber节点深度绑定
    • 通过alternate实现状态热更新
  4. 副作用处理

    • 通过effectTag标记DOM操作
    • 提交阶段批量处理副作用(Commit Phase)

此架构实现了React核心特性(Fiber架构、Hooks、协调算法)的简化版实现,适合大家用于理解现代React的内部工作原理。


一、第一步:编写JSX 解析:jsx.ts

算法设计思路:

createTextElement 流程
对象
原始值
createTextElement 开始
调用 createTextElement
接收 text 参数
创建 TEXT_ELEMENT 类型对象
设置 nodeValue 属性
children 设为空数组
创建文本节点
createElement 开始
接收 type, props, ...children 参数
children 存在?
遍历处理子元素
当前子元素类型?
直接保留
收集处理后的子元素
children 设为空数组
合并 props 和标准化后的 children
返回虚拟DOM对象

(老曹建议手动coding实现思路)

[开始] → [调用createElement]
       ├─ [创建DOM对象]
       ├─ [处理props]
       ├─ [遍历children]
       │   ├─ [判断child类型]
       │   │   ├─ [对象 → 保留] → [收集]
       │   │   └─ [原始值 → 调用createTextElement]
       │   │        └─ [创建TEXT_ELEMENT节点] → [收集]
       │   └─ [合并children]
       └─ [返回虚拟DOM] → [结束]

算法详细实现:

// react/jsx.ts

// 导出创建虚拟DOM元素的函数
export function createElement(
  type: string | Function,   // 元素类型(可以是HTML标签名或自定义组件函数)
  props: any,               // 元素属性对象(包含className、style等)
  ...children: any[]        // 剩余参数形式的子元素(可以是字符串、数字或其他元素)
) {
  return {
    type,  // 元素类型(与参数type对应)
    props: {
      // 合并原始props和自动处理的children
      ...props,
      // 标准化处理子元素:将非对象类型的子元素转换为文本元素
      children: children.map(child =>
        // 如果子元素是对象(如其他虚拟DOM元素),则直接保留
        // 如果是原始值(字符串/数字),则创建对应的文本节点
        typeof child === 'object' ? child : createTextElement(child)
      ),
    },
  };
}

// 创建文本虚拟DOM元素的专用函数
function createTextElement(text: string) {
  return {
    type: 'TEXT_ELEMENT',       // 标识这是一个文本元素类型
    props: {
      nodeValue: text,          // 存储文本内容(类似DOM的nodeValue属性)
      children: [],             // 文本元素没有子元素(始终为空数组)
    },
  };
}

index.tsx 中使用:

/** @jsxRuntime classic */
/** @jsx createElement */
import { createElement } from './react/jsx';

关键点说明:

  1. 类型系统:使用TypeScript类型标注,type参数可以是字符串(HTML标签)或函数(组件)
  2. 属性合并:通过对象展开运算符...props保留原始属性,同时添加标准化处理的children
  3. 元素标准化:所有子元素都会被转换为对象形式,保证虚拟DOM结构的统一性
  4. 文本元素特殊处理:通过TEXT_ELEMENT类型区分普通元素和文本节点,文本内容存储在nodeValue属性中
  5. 剩余参数:使用...children收集所有子元素参数,支持可变数量的子元素

这种设计模式常用于虚拟DOM实现(比如React-like库),通过标准化元素结构,为后续的DOM diff和 reconciliation 过程提供便利。


二、第二步:Virtual DOM 构建:vdom.ts

// 定义虚拟DOM节点的类型别名
export type VNode = {
  // 元素类型:可以是HTML标签名(如'div')或组件函数
  type: string | Function;
  
  // 元素属性对象:使用Record类型表示键值对结构
  // 键为属性名(字符串类型),值为任意类型(支持所有属性类型)
  props: Record<string, any>;
  
  // 子元素数组:递归类型定义,子元素必须是VNode类型
  // 形成树形结构,每个节点都可以包含子节点
  children: VNode[];
};

类型定义解析:

  1. VNode 是虚拟DOM(Virtual Node)的核心类型定义
  2. type 字段:
    • 字符串类型:对应HTML原生标签(如 ‘div’, ‘span’)
    • 函数类型:对应React-like组件(函数组件或类组件)
  3. props 字段:
    • 使用TypeScript内置的 Record 工具类型
    • 等价于 { [key: string]: any },表示键值对形式的属性集合
    • 包含className、style、事件处理函数等所有属性
  4. children 字段:
    • 递归类型定义,保证类型安全
    • 每个子元素都必须是符合VNode类型的虚拟节点
    • 形成DOM树结构,支持无限层级嵌套

这个类型定义与之前实现的 createElement 函数完美匹配,共同构成了虚拟DOM的基础结构:

  • createElement 函数返回的值符合VNode类型
  • type 对应组件/标签类型
  • props 包含所有属性及处理后的子元素
  • children 经过标准化处理为VNode数组

这种设计模式是现代前端框架(如React、Preact)实现虚拟DOM的核心机制,通过类型系统保证节点结构的可预测性,同时为后续的DOM diff算法和协调过程(Reconciliation)提供类型安全的基础。


三、第三步:Fiber 构建与渲染:fiber.ts

React Fiber 架构核心数据结构及渲染入口的算法流程图解析(使用 Mermaid 语法):

属性设置流程
createDom 流程
遍历props键值对
是否为保留属性?
设置DOM属性
跳过children属性
返回处理后节点
是否为文本元素?
createDom 开始
创建文本节点
创建普通元素节点
初始化空文本节点
创建指定类型元素
设置节点属性
过滤并复制props
排除children属性
返回DOM节点
render 开始
创建初始根Fiber节点
设置 wipRoot 属性
关联容器DOM节点
初始化 props.children
设置 nextUnitOfWork 指针
启动协调过程

流程图解析:

  1. 渲染入口流程(render)

    • 紫色节点:函数入口/出口
    • 绿色节点:Fiber节点创建
    • 黄色节点:协调过程启动
  2. DOM创建流程(createDom)

    • 紫色节点:函数入口/出口
    • 蓝色节点:条件判断
    • 绿色节点:节点创建
    • 黄色节点:属性处理
  3. 属性设置流程

    • 蓝色节点:循环控制
    • 绿色节点:属性赋值
    • 紫色节点:条件过滤

核心数据结构说明:

  1. Fiber节点结构

    type Fiber = {
      type?: any;                // 元素类型标识
      key?: any;                 // 列表重组标识
      props?: Record<string, any>; // 属性对象
      dom: HTMLElement | Text | null; // 关联DOM节点
      parent: Fiber | null;      // 父节点指针
      child: Fiber | null;       // 子节点链表
      sibling: Fiber | null;     // 兄弟节点链表
      alternate?: Fiber | null;  // 双缓冲指针
      effectTag?: EffectTypes;   // 副作用标记
      hooks?: HookState[];       // 函数组件状态
    }
    
  2. 双缓冲机制

    • alternate 指针实现时间分片更新
    • 工作进度树(wipRoot)与当前树(currentRoot)交替渲染
  3. 副作用系统

    • 通过 effectTag 标记需要执行的DOM操作
    • 支持 UPDATE/PLACEMENT/DELETION 三种操作类型
  4. 链表结构

    • 父子关系通过 child 指针建立
    • 兄弟关系通过 sibling 指针建立
    • 形成树形结构的链表遍历路径
开始
│
├─ 调用 render(element, container)
│   │
│   ├─ 创建初始根Fiber节点 (wipRoot)
│   │   ├─ dom: 关联容器
│   │   ├─ props.children: [element]
│   │   └─ parent: null
│   │
│   └─ 设置 nextUnitOfWork = wipRoot
│
├─ 进入协调循环 (while(nextUnitOfWork !== null))
│   │
│   ├─ 处理当前工作单元 (nextUnitOfWork)
│   │   │
│   │   ├─ 调用 createDom(fiber)
│   │   │   │
│   │   │   ├─ 判断是否为文本元素 (type === 'TEXT_ELEMENT')
│   │   │   │   ├─ 是 → 创建文本节点 document.createTextNode('')
│   │   │   │   └─ 否 → 创建元素节点 document.createElement(type)
│   │   │   │
│   │   │   └─ 设置DOM属性
│   │   │        ├─ 过滤props.children
│   │   │        └─ 遍历剩余属性设置到DOM
│   │   │
│   │   └─ 处理子节点 (fiber.child)
│   │        ├─ 存在子节点 → nextUnitOfWork = child
│   │        └─ 无子节点 → 处理兄弟节点 (fiber.sibling)
│   │             ├─ 存在兄弟节点 → nextUnitOfWork = sibling
│   │             └─ 回溯父节点 (fiber.parent)
│   │                  └─ 继续向上查找兄弟节点
│   │
│   └─ 协调循环继续...
│
├─ 协调完成 (nextUnitOfWork === null)
│   │
│   ├─ 提交阶段 (commitPhase)
│   │   ├─ 将wipRoot的DOM树插入容器 (container.appendChild(wipRoot.dom))
│   │   └─ 更新currentRoot = wipRoot
│   │
│   └─ 重置wipRoot = null
│
结束

关键流程节点说明:

  1. 初始化阶段

    • 创建根Fiber节点(wipRoot)
    • 建立与容器DOM的关联
    • 设置初始props.children
  2. 协调循环

    • 采用深度优先遍历策略
    • 工作单元指针(nextUnitOfWork)驱动流程
    • 包含三个主要处理方向:
      • ✅ 子节点优先(child优先)
      • ➡️ 兄弟节点次之(sibling)
      • ↩️ 父节点回溯(parent)
  3. DOM创建

    • 文本节点特殊处理(TEXT_ELEMENT标识)
    • 属性设置过滤机制(排除children属性)
    • 类型断言处理(TypeScript类型兼容)
  4. 提交阶段

    • 批量DOM操作(实际React中分阶段提交)
    • 双缓冲切换(currentRoot ↔ wipRoot)
    • 清理工作进阶树(wipRoot重置)

简化记忆版:
(老曹建议手动coding实现思路)

[开始] → [render()]
       ├─ [创建wipRoot]
       ├─ [设置nextUnitOfWork]
       ├─ [协调循环]
       │   ├─ [处理当前Fiber]
       │   │   ├─ [createDom()]
       │   │   │   ├─ [文本节点?]
       │   │   │   ├─ [创建元素节点]
       │   │   │   └─ [设置属性]
       │   │   ├─ [处理child]
       │   │   ├─ [处理sibling]
       │   │   └─ [回溯parent]
       │   └─ [循环条件判断]
       ├─ [提交阶段]
       │   ├─ [DOM插入容器]
       │   └─ [更新currentRoot]
       └─ [结束]

渲染算法实现:

// 定义Fiber节点类型(React Fiber架构的核心数据结构)
export type Fiber = {
  type?: any;               // 元素类型(标签名/组件函数/文本类型等)
  key?: any;                // 用于列表重组的唯一标识
  props?: Record<string, any>; // 元素属性对象
  dom: HTMLElement | Text | null; // 对应的真实DOM节点(文本节点或元素节点)
  parent: Fiber | null;     // 父级Fiber节点(形成树形结构)
  child: Fiber | null;      // 第一个子节点
  sibling: Fiber | null;    // 下一个兄弟节点
  alternate?: Fiber | null; // 对应更新前的Fiber节点(用于双缓冲技术)
  effectTag?: 'UPDATE' | 'PLACEMENT' | 'DELETION'; // 副作用标识(增/删/改)
  hooks?: HookState[];      // 函数组件的钩子状态数组
};

// 当前已提交的根Fiber节点(对应已渲染的DOM树)
export let currentRoot: Fiber | null = null;
// 正在构建中的工作进阶根Fiber节点(对应未提交的更新)
export let wipRoot: Fiber | null = null;

// 协调器的工作单元指针(指向下一个需要处理的Fiber节点)
let nextUnitOfWork: Fiber | null = null;

/**
 * 根据Fiber节点创建真实DOM元素
 * @param fiber - 要创建DOM的Fiber节点
 * @returns 创建的DOM元素(文本节点或普通元素)
 */
export function createDom(fiber: Fiber): HTMLElement | Text {
  // 判断是否为文本元素(通过type标识)
  const isTextElement = fiber.type === 'TEXT_ELEMENT';
  // 创建对应DOM节点(文本节点或普通元素)
  const dom = isTextElement
    ? document.createTextNode('')  // 创建空文本节点
    : document.createElement(fiber.type as string); // 类型断言为string

  // 过滤并设置属性(排除children属性)
  const isProperty = (key: string) => key !== 'children';
  Object.keys(fiber.props || {})  // 防止props为undefined
    .filter(isProperty)
    .forEach(key => {
      // 使用类型断言绕过TypeScript的类型检查(实际开发中建议更严谨的类型处理)
      (dom as any)[key] = fiber.props[key];
    });

  return dom;
}

/**
 * 渲染入口函数(启动协调过程)
 * @param element - 要渲染的虚拟DOM元素
 * @param container - 容器DOM元素
 */
export function render(element: any, container: HTMLElement) {
  // 构建初始根Fiber节点(工作进阶根节点)
  wipRoot = {
    dom: container,                   // 关联容器DOM
    props: {
      children: [element],            // 初始子元素(要渲染的虚拟DOM)
    },
    parent: null,                     // 根节点无父节点
    child: null,                      // 初始无子节点
    sibling: null,                    // 初始无兄弟节点
  };

  nextUnitOfWork = wipRoot;           // 启动协调过程
}

第四步:Reconciler 协调器:reconciler.ts

以下是 React 协调器核心算法的流程图解析:

reconcileChildren 流程
初始化 index=0, oldFiber=父节点.alternate?.child
协调子节点 reconcileChildren
循环对比新旧子节点
新旧节点类型相同?
复用旧节点, 更新属性
存在新节点?
创建新节点, 标记PLACEMENT
存在旧节点?
标记旧节点DELETION
构建新Fiber链表
index++, 移动旧Fiber指针
performUnitOfWork 开始
fiber.dom 存在?
创建DOM节点 createDom
跳过创建
fiber.child 存在?
返回 fiber.child
寻找下一个工作单元
当前节点有兄弟?
返回兄弟节点
回溯到父节点
父节点存在?
所有节点处理完成

算法流程解析:

  1. performUnitOfWork 主流程

    • 紫色节点:条件判断分支
    • 绿色节点:DOM操作
    • 黄色节点:工作单元返回
    • 蓝色节点:流程控制
  2. reconcileChildren 协调流程

    • 蓝色节点:循环控制
    • 绿色节点:节点创建/更新
    • 黄色节点:标记操作
    • 紫色节点:条件判断

核心逻辑说明:

  1. 双缓冲技术:通过alternate指针复用旧Fiber树,实现增量更新
  2. 三色标记法
    • PLACEMENT(新增)
    • UPDATE(更新)
    • DELETION(删除)
  3. 遍历策略
    • 深度优先遍历(优先处理子节点)
    • 回溯机制(处理兄弟节点)
  4. Diff算法
    • 单节点对比(Key机制优化)
    • 链表指针移动策略
    • 位置敏感型对比(移动/插入/删除)

算法代码实现:

// react/reconciler.ts
// React 协调器核心实现(基于Fiber架构)

import { Fiber, createDom } from './fiber';

/**
 * 执行单个工作单元(Fiber节点)
 * @param fiber - 当前要处理的Fiber节点
 * @returns 下一个需要处理的Fiber节点(形成链表遍历)
 */
export function performUnitOfWork(fiber: Fiber): Fiber | null {
  // 1. 创建DOM节点(仅首次渲染时执行)
  if (!fiber.dom && fiber.type) {
    fiber.dom = createDom(fiber);
  }

  // 2. 协调子节点(对比新旧虚拟DOM)
  const elements = fiber.props?.children || [];
  reconcileChildren(fiber, elements);

  // 3. 返回下一个工作单元(深度优先遍历)
  if (fiber.child) {
    return fiber.child; // 优先处理子节点
  }

  // 子节点处理完成后,回溯处理兄弟节点
  let next: Fiber | null = fiber;
  while (next) {
    if (next.sibling) {
      return next.sibling; // 存在兄弟节点则返回
    }
    next = next.parent; // 回溯到父节点继续查找
  }

  return null; // 所有节点处理完成
}

/**
 * 协调子节点(Diff算法核心)
 * @param wipFiber - 当前正在构建的Fiber节点(WorkInProgress)
 * @param elements - 新的虚拟DOM元素数组
 */
function reconcileChildren(wipFiber: Fiber, elements: Fiber[]) {
  let index = 0;
  let oldFiber = wipFiber.alternate?.child; // 获取旧Fiber链表的第一个子节点
  let prevSibling: Fiber | null = null;

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

    // 类型相同:复用旧Fiber节点(双缓冲技术)
    const sameType = oldFiber && element && element.type === oldFiber.type;

    if (sameType) {
      // 更新节点(保留旧DOM引用)
      newFiber = {
        type: oldFiber!.type,
        props: element.props,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
        dom: oldFiber!.dom, // 复用旧DOM节点
        child: null,
        sibling: null,
        hooks: [],
      };
    } 
    // 新增节点(element存在但oldFiber不存在)
    else if (element) {
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT', // 标记为插入操作
        dom: null,
        child: null,
        sibling: null,
        hooks: [],
      };
    } 
    // 删除节点(oldFiber存在但element不存在)
    else if (oldFiber) {
      oldFiber.effectTag = 'DELETION'; // 标记为删除操作
    }

    // 移动旧Fiber指针
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    // 构建新Fiber链表
    if (index === 0) {
      wipFiber.child = newFiber; // 第一个子节点
    } else if (element && prevSibling) {
      prevSibling.sibling = newFiber; // 兄弟节点连接
    }

    prevSibling = newFiber; // 记录前一个兄弟节点
    index++;
  }
}

关键实现细节说明:

  1. 双缓冲技术
  • 通过alternate属性关联新旧Fiber树
  • 更新时复用旧DOM节点(dom: oldFiber!.dom
  • 保持内存中同时存在两棵Fiber树(currentRoot/wipRoot)
  1. 协调算法流程
  • 遍历新旧子节点列表(双指针法)
  • 三种操作类型:
    • UPDATE:复用旧节点,更新属性
    • PLACEMENT:创建新节点,插入DOM
    • DELETION:标记删除旧节点
  1. Fiber链表构建
  • 通过childsibling形成单向链表
  • 深度优先遍历顺序(先子节点后兄弟节点)
  • 回溯机制通过parent指针实现
  1. 副作用标记
  • effectTag用于提交阶段确定需要执行的DOM操作
  • 删除操作通过异步批量处理(需配合commit阶段实现)
  1. 性能优化
  • 类型相同即复用(避免不必要的DOM操作)
  • 键值(key)优化:实际实现中需要依赖key进行精确匹配
  • 异步调度:通过nextUnitOfWork实现可中断的协调过程

需要补充的完整功能:

  1. 键值(key)处理逻辑
  2. 函数组件协调
  3. 钩子状态更新
  4. 副作用收集与提交
  5. 生命周期方法调用
  6. 事件系统绑定

实现了React Fiber架构的核心协调机制,通过链表遍历和双缓冲技术实现了高效的可中断渲染流程。


第五步:调度器:scheduler.ts

// react/scheduler.ts
// React 调度器核心实现(基于requestIdleCallback)

import { Fiber, wipRoot, currentRoot, nextUnitOfWork } from './fiber';
import { performUnitOfWork } from './reconciler';

let workInProgressFiber: Fiber | null = null; // 当前正在处理的Fiber节点
let hookIndex = 0; // Hooks状态索引(用于函数组件)

/**
 * 调度器工作循环(浏览器空闲时执行)
 * @param deadline - 浏览器空闲回调参数(含剩余时间信息)
 */
export function workLoop(deadline: IdleDeadline) {
  let shouldYield = false; // 是否需要让出主线程标志

  // 持续处理Fiber任务直到:
  // 1. 没有更多任务(nextUnitOfWork为空)
  // 2. 浏览器需要重新渲染(剩余时间<1ms)
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行单个工作单元
    shouldYield = deadline.timeRemaining() < 1; // 检查剩余时间
  }

  // 所有任务处理完成时提交更新
  if (!nextUnitOfWork && wipRoot) {
    commitRoot(); // 提交阶段:将效果应用到真实DOM
  }

  requestIdleCallback(workLoop); // 注册下一个空闲回调
}

// 启动调度器(立即执行首次调度)
requestIdleCallback(workLoop);

/**
 * 提交阶段:将工作进度根节点的效果应用到DOM
 */
function commitRoot() {
  commitWork(wipRoot!.child); // 从根节点的第一个子节点开始提交
  currentRoot = wipRoot; // 更新当前根节点引用
  wipRoot = null; // 清空工作进度根节点
}

/**
 * 递归提交Fiber节点效果到DOM
 * @param fiber - 当前要提交的Fiber节点
 */
function commitWork(fiber: Fiber | null) {
  if (!fiber) return;

  const parentFiber = fiber.parent!; // 获取父Fiber节点
  const domParent = parentFiber.dom!; // 获取父DOM节点(必存在)

  // 根据effectTag执行不同操作
  if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
    // 插入新节点
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
    // 更新现有节点属性
    updateDom(fiber.dom, fiber.alternate!.props, fiber.props);
  } else if (fiber.effectTag === 'DELETION') {
    // 删除节点(递归处理子节点)
    commitDeletion(fiber, domParent);
  }

  // 递归提交子节点和兄弟节点
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

/**
 * DOM属性更新器(处理事件监听和属性变更)
 * @param dom - 要更新的DOM元素
 * @param prevProps - 旧属性
 * @param nextProps - 新属性
 */
function updateDom(dom: HTMLElement | Text, prevProps: any, nextProps: any) {
  // 辅助函数:判断是否为事件属性
  const isEvent = (key: string) => key.startsWith('on');
  // 辅助函数:判断是否为普通属性(排除children和事件)
  const isProperty = (key: string) => key !== 'children' && !isEvent(key);
  // 辅助函数:判断属性值是否变化
  const isNew = (key: string) => (prev: any, next: any) => prev[key] !== next[key];

  // 移除旧事件监听
  Object.keys(prevProps)
    .filter(isEvent)
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2); // 转换onClick为click
      dom.removeEventListener(eventType, prevProps[name]);
    });

  // 添加新事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });

  // 删除已变更的旧属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      (dom as any)[name] = undefined; // 清除旧属性
    });

  // 设置新属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      (dom as any)[name] = nextProps[name]; // 更新属性值
    });
}

/**
 * 递归删除节点及其子节点
 * @param fiber - 要删除的Fiber节点
 * @param domParent - 父DOM节点
 */
function commitDeletion(fiber: Fiber, domParent: HTMLElement | Text) {
  if (fiber.dom) {
    // 直接删除有DOM节点的Fiber
    domParent.removeChild(fiber.dom);
  } else if (fiber.child) {
    // 递归删除子节点(针对没有DOM的组件节点)
    commitDeletion(fiber.child, domParent);
  }
}

算法流程图(Mermaid 格式)

updateDom 流程
commitWork 流程
PLACEMENT
UPDATE
DELETION
事件监听处理
旧事件移除
新事件添加
属性变更检测
删除旧属性
设置新属性
返回
当前节点存在?
commitWork 递归提交
返回
effectTag 类型?
DOM插入操作
DOM属性更新
DOM删除操作
递归处理子节点
递归处理兄弟节点
workLoop 开始
nextUnitOfWork 存在?
执行 performUnitOfWork
剩余时间 <1ms?
shouldYield=true
提交阶段 commitRoot
需要继续任务?
让出主线程
清空 wipRoot
注册下一个空闲回调

核心机制说明:

  1. 调度器特性

    • 基于requestIdleCallback的协作式调度
    • 时间分片(time slicing)机制
    • 优先级队列管理(通过nextUnitOfWork链表)
  2. 提交阶段特点

    • 三阶段提交:插入/更新/删除
    • 深度优先遍历(DOM操作顺序保证)
    • 事件系统解绑/重绑机制
  3. 优化策略

    • 双缓冲技术(通过alternate指针)
    • 批量属性更新(减少DOM操作次数)
    • 递归删除优化(自动处理子节点)
  4. 关键数据结构

    • wipRoot(工作进度根节点)
    • currentRoot(当前已提交根节点)
    • effectTag(操作标记系统)

该实现展示了React 18+并发模式的核心调度逻辑,通过将渲染工作分解为可中断的小任务单元,实现了对浏览器主线程的精细控制。


第六步:Hook 实现:hook.ts

// react/hook.ts
// React Hooks 核心实现(基于Fiber架构)

import { Fiber } from './fiber';

// Hooks 状态类型定义
export type HookState = {
  queue: (() => void)[];     // 更新队列(存储待执行的更新函数)
  pending: any;              // 待处理的更新(用于批量处理)
  memoizedState: any;        // 记忆化状态值
  next: HookState | null;    // 下一个Hook的链表指针
  deps?: any[];              // 依赖项数组(useEffect专用)
  callback?: () => void;     // 副作用回调函数(useEffect专用)
  cleanup?: () => void;      // 清理函数(useEffect专用)
};

// Hooks 上下文类型定义
export type HookContext = {
  fiber: Fiber;             // 当前渲染的Fiber节点
  hookIndex: number;        // 当前Hook的索引位置
};

// 全局状态:当前正在渲染的Fiber节点
export let currentlyRenderingFiber: Fiber | null = null;

/**
 * useState 钩子实现
 * @param initial - 状态初始值
 * @returns [当前状态, 状态更新函数]
 */
export function useState<T>(initial: T): [T, (newState: T) => void] {
  const fiber = currentlyRenderingFiber!; // 获取当前渲染的Fiber
  const hook = getHook(); // 获取当前Hook实例(或创建新Hook)

  // 初始化状态(仅在首次渲染时执行)
  if (!hook.alternate) {
    hook.memoizedState = initial;
  }

  // 定义状态更新函数
  const setState = (action: T) => {
    // 将更新推入队列(支持函数式更新)
    hook.queue.push(() => {
      hook.memoizedState = action; // 直接更新记忆化状态
    });

    rerender(fiber); // 触发重新渲染
  };

  return [hook.memoizedState, setState]; // 返回状态和更新器
}

/**
 * useEffect 钩子实现
 * @param callback - 副作用回调函数
 * @param deps - 依赖项数组
 */
export function useEffect(callback: () => void | (() => void), deps: any[]) {
  const fiber = currentlyRenderingFiber!;
  const hook = getHook();

  // 获取旧依赖项(首次渲染时为undefined)
  const oldDeps = hook.deps;
  // 深度比较依赖项变化
  const hasChanged = !oldDeps || deps.some((dep, i) => dep !== oldDeps![i]);

  if (hasChanged) {
    // 依赖变化时:
    hook.callback = callback;   // 更新回调函数
    hook.cleanup = undefined;   // 重置清理函数
    hook.deps = deps;           // 存储新依赖项
  }
}

/**
 * 获取当前Hook实例(或创建新Hook)
 * @returns 当前Fiber节点对应的Hook实例
 */
function getHook() {
  const fiber = currentlyRenderingFiber!;
  const hookIndex = fiber.hookIndex; // 获取当前Hook索引
  
  // 获取当前索引位置的Hook(首次渲染时需要创建)
  let hook = fiber.hooks![hookIndex];

  if (!hook) {
    // 创建新Hook实例
    hook = {
      memoizedState: null,    // 初始状态为null
      queue: [],              // 初始化空更新队列
      pending: null,          // 初始无待处理更新
      next: null,             // 链表指针初始化
      deps: null,             // 初始无依赖项
      callback: null,         // 初始无回调
      cleanup: null,          // 初始无清理函数
    };
    fiber.hooks!.push(hook);  // 将新Hook加入链表
  } else {
    fiber.hookIndex++;        // 非首次渲染时递增索引
  }

  return hook;
}

/**
 * 触发重新渲染(调度协调过程)
 * @param fiber - 需要重新渲染的Fiber节点
 */
function rerender(fiber: Fiber) {
  // 创建虚拟根节点(用于启动新协调过程)
  const root = {
    dom: fiber.dom,           // 复用原容器DOM
    props: fiber.props,       // 复用原props
    alternate: fiber,         // 指向旧Fiber树(双缓冲)
  };

  nextUnitOfWork = root;      // 设置新的工作单元起点
}

核心机制说明:

  1. Hooks 链表管理

    • 通过 fiber.hooks 数组维护函数组件的Hooks链表
    • 使用 hookIndex 指针实现线性遍历
    • 每个Hook通过 next 指针形成单向链表
  2. 状态持久化

    • alternate 指针实现双缓冲技术
    • 更新时复用旧Hook的 memoizedState
    • 批量更新通过 queue 数组实现
  3. 副作用系统

    • 依赖项比较采用深度比较策略
    • 清理函数通过 cleanup 字段管理
    • 回调函数与清理函数形成配对机制
  4. 并发更新

    • rerender 函数创建虚拟根节点启动新协调过程
    • 通过 nextUnitOfWork 指针实现任务调度
    • 保持与浏览器空闲回调的集成能力

我们实现了React函数组件的核心工作原理,包括:

  • Hooks数据结构的组织方式
  • 状态更新的原子性保证
  • 副作用的生命周期管理
  • 与Fiber架构的深度集成
  • 并发模式的支持基础

第七步:主入口:index.tsx

/** @jsx createElement */
import { createElement } from './react/jsx';
import { render } from './react/fiber';
import { useState } from './react/hook';

function Counter() {
  const [state, setState] = useState(0);

  return (
    <div>
      <h1>{state}</h1>
      <button onClick={() => setState(state + 1)}>+1</button>
    </div>
  );
}

const element = <Counter />;
const container = document.getElementById('root');

if (container) {
  render(element, container);
}

第八步:HTML 页面:index.html

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Mini Reacttitle>
head>
<body>
  <div id="root">div>
  <script src="./dist/index.js">script>
body>
html>

第九步:编译配置建议

安装依赖

1. 安装依赖命令

npm install typescript ts-node webpack webpack-cli html-webpack-plugin --save-dev
  • typescript: TypeScript 编译器
  • ts-node: 在 Node.js 中直接执行 TypeScript 代码
  • webpack: 模块打包工具
  • webpack-cli: Webpack 命令行工具
  • html-webpack-plugin: 自动生成 HTML 文件并注入打包后的脚本
  • --save-dev: 标记为开发依赖

2. tsconfig.json 配置

{
  "compilerOptions": {
    "target": "ESNext",          // 编译目标为最新 ES 规范(支持最新语法特性)
    "module": "ESNext",          // 使用 ES 模块系统(与 Webpack 兼容)
    "esModuleInterop": true,     // 允许 CommonJS 和 ES 模块互操作(解决默认导入问题)
    "outDir": "./dist",          // 编译输出目录
    "jsx": "react-jsx"           // 启用 React JSX 编译(需 TypeScript 4.1+)
  },
  "include": ["./"]              // 包含当前目录下所有文件(可根据需要调整)
}

3. webpack.config.js 配置

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入 HTML 模板插件

module.exports = {
  entry: './index.tsx',          // 入口文件(React 应用主入口)
  output: {
    filename: 'bundle.js',       // 输出文件名(打包后的 JS 文件)
    path: __dirname,             // 输出目录(当前目录,建议改为 './dist')
    publicPath: '/',             // 资源基础路径(用于开发服务器)
    clean: true                  // 构建前清理输出目录(Webpack 5+ 特性)
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'], // 自动解析的扩展名(按顺序尝试)
    alias: {                     // 路径别名配置(示例)
      '@': path.resolve(__dirname, 'src/')
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,         // 匹配所有 .ts 和 .tsx 文件
        use: 'ts-loader',        // 使用 ts-loader 处理 TypeScript
        exclude: /node_modules/, // 排除 node_modules 目录
        include: [path.resolve(__dirname, "src")] // 仅处理 src 目录(可选优化)
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',  // 指定 HTML 模板路径
      inject: 'body',            // 脚本注入位置(body 底部)
      hash: true,                // 添加版本哈希(防止缓存)
      minify: {                  // 生产环境压缩配置
        removeComments: true,
        collapseWhitespace: true
      }
    })
  ],
  devtool: 'source-map',         // 生成 source map(开发环境推荐)
  devServer: {                   // 开发服务器配置
    static: './dist',            // 静态文件目录
    port: 3000,                  // 开发服务器端口
    hot: true,                   // 启用 HMR(热模块替换)
    open: true                   // 自动打开浏览器
  },
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' // 环境判断
};

参考:【webpack】Webpack 实战配置教程(最全版):涵盖所有配置项 + 性能优化建议


配置说明

  1. TypeScript 配置亮点

    • ESNext 目标确保使用最新 JavaScript 特性
    • esModuleInterop 解决 import/require 兼容问题
    • 建议添加 "jsx": "react-jsx" 以支持 JSX 编译
  2. Webpack 核心配置

    • 入口/输出:从 index.tsx 开始构建,输出到 dist 目录
    • 模块解析:自动处理 .ts/.tsx 文件,排除 node_modules
    • HTML 插件:自动生成包含打包脚本的 HTML 文件
    • 开发服务器:集成 HMR 和自动打开浏览器功能
  3. 环境区分

    • 开发环境:devtool: 'eval-cheap-module-source-map'
    • 生产环境:mode: 'production' + 代码分割配置

建议大家根据实际项目需求调整路径和插件配置,此配置已包含 React 开发所需的基本功能。


十、总结

✅ 本项目实现了如下核心模块:

模块 内容
JSX 解析 自定义 createElement 处理 JSX
Virtual DOM 用于描述组件树
Fiber 架构 支持递归构建 Fiber 树
协调器 Diff 算法处理新增/更新/删除
调度器 利用 requestIdleCallback 实现时间切片
Hook 系统 支持 useStateuseEffect 的基础逻辑

参考源码笔记:
【React源码01】深入学习React 源码实现——JSX与虚拟DOM生成
【React源码02】深入学习React 源码实现——虚拟 DOM(Virtual DOM)与 Diffing 算法
【React源码03】深入学习React 源码实现——Fiber 架构与任务调度
【React源码04】深入学习React 源码实现—— React Hooks 的底层实现原理(useState,useEffect,useRef)
【React源码08】深入学习React 源码实现——Fiber架构双缓冲(current 与 workInProgress)机制
【React源码10】深入学习React 源码实现——优先级Lane模型
【React源码11】深入学习React 源码实现——调度器(Scheduler)底层实现
【React源码12】深入学习React 源码实现——Scheduler 与 Fiber 的协同工作机制
【React源码13】深入学习React 源码实现——如何自定义 React 的调度策略(Custom Scheduler Strategy)

十一、扩展方向(进阶学习)

方向 建议
支持函数组件 增加对 renderWithHooks 的封装
支持 Effect 清理 在 commit 阶段执行 cleanup 函数
支持 Hooks 异常边界 类似 getDerivedStateFromError
支持并发模式 引入 Lane 优先级系统
支持 Suspense 实现异步加载状态管理
支持 Context API 实现上下文传递机制

参考源码笔记:
【React源码09】深入学习React 源码实现——Fiber架构副作用收集Effect List
【React源码32】深入学习React 源码实现——React 错误边界(Error Boundaries)与异常捕获机制
【React源码06】深入学习React 源码实现—— Context API底层实现
【React源码26】深入学习React 源码实现——React.lazy 与 Suspense 的底层实现

十二、结语

通过我们本次实战演练,大家已经掌握了 React 最核心的底层原理和实现方式。尽管这是一个极简版本,但它完整展示了:

  • 如何解析 JSX;
  • 如何构建 Virtual DOM;
  • 如何利用 Fiber 架构实现协调与渲染;
  • 如何实现最简单的 Hook 系统;
  • 如何调度异步渲染任务;

这套知识体系是理解 React 源码、提升前端架构能力的重要一步。

如果你希望继续深入学习,可以参考老曹的源码系列文章,尝试:

  • 实现完整的 useEffect 清理逻辑;
  • 支持 useReduceruseRef
  • 实现 Suspense
  • 引入并发调度 Lane 机制;
  • 接入 DevTools 调试支持;

在此,我们的react源码第一阶段学习到此完美收官,祝你在 React 源码探索之路上越走越远!

你可能感兴趣的:(源码学习笔记,react.js,typescript,javascript,前端,源码,手写react,react)