首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【卡颂】从0实现React18:手写并发调度引擎与Fiber架构

【卡颂】从0实现React18:手写并发调度引擎与Fiber架构

原创
作者头像
用户12678265
发布2026-08-21 13:48:42
发布2026-08-21 13:48:42
1060
举报

【卡颂】从0实现React18:手写并发调度引擎与Fiber架构

摒弃官腔,直击灵魂。本文不教你怎么用React,而是带你一个React 18。我们将剥离JSX语法糖,用~300行核心TypeScript代码,实现包含 并发调度(Concurrent Scheduler)Fiber双缓冲架构 以及 Hooks链表 的迷你React核心库。全篇硬核,无一句废话。


1. 破局:为什么React18必须从Fiber开始?

React 15及之前的Stack Reconciler采用递归同步渲染,一旦开始便不可中断。React18的杀手锏是可中断的异步渲染,其底层依赖两大核心设计:

  • Fiber架构:将虚拟DOM转换为链表结构(Fiber Node),支持“断点续传”。
  • Lane优先级模型:用二进制位表示不同优先级(如SyncLaneDefaultLane),实现高优先级任务插队。

本文将严格模拟React内部三大模块:

  1. Scheduler(调度器)—— 时间切片与任务优先级。
  2. Reconciler(协调器)—— Fiber树的构建与Diff。
  3. Renderer(渲染器)—— 将Fiber树映射为真实DOM。

2. 基石:定义Fiber节点与全局状态

我们使用TypeScript定义核心数据结构。React18最大的变化在于双缓冲树currentworkInProgress)。

代码语言:javascript
复制
// 工作单元(Fiber节点)
export interface Fiber {
  // 节点标识
  tag: WorkTag; // 0=FunctionComponent, 1=HostRoot, 5=HostComponent, 6=HostText
  key: string | null;
  type: any; // 函数体或DOM标签名
  
  // 链表结构(核心!)
  child: Fiber | null;
  sibling: Fiber | null;
  return: Fiber | null; // 父级
  
  // 状态与副作用
  pendingProps: any;
  memoizedState: any; // 对于FC,这里挂载Hooks链表
  memoizedProps: any;
  updateQueue: UpdateQueue<any> | null;
  
  // 双缓冲关联
  alternate: Fiber | null; // 指向另一棵树的对应节点
  
  // DOM实例(HostComponent对应真实节点)
  stateNode: any;
  
  // 副作用标志
  flags: Flags; // 如 Placement(2), Update(4), Deletion(8)
  subtreeFlags: Flags;
  deletions: Fiber[] | null;
  
  // 优先级调度相关
  lanes: Lanes;
  childLanes: Lanes;
}

// 根节点(FiberRoot)
export interface FiberRoot {
  current: Fiber; // 指向当前已渲染的fiber树
  container: HTMLElement | null;
  pendingLanes: Lanes;
  finishedLane: Lane;
  callbackNode: any;
  callbackPriority: number;
}

关键点alternate是复用缓存的关键。每次更新时,React会克隆current树作为workInProgress,所有变更在workInProgress上操作,完成后再通过finishedWork切换指针。


3. 调度器(Scheduler)—— 时间切片与优先级队列

React18不再依赖requestIdleCallback,而是自建调度器,核心利用MessageChannel实现宏任务,以避开浏览器渲染帧的阻塞。

3.1 优先级定义与任务结构

代码语言:javascript
复制
export const NoPriority = 0;
export const ImmediatePriority = 1; // 同步阻塞
export const UserBlockingPriority = 2; // 用户交互(点击、输入)
export const NormalPriority = 3; // 默认
export const IdlePriority = 5; // 空闲

interface Task {
  id: number;
  callback: (didTimeout: boolean) => void;
  priorityLevel: number;
  startTime: number;
  expirationTime: number;
  sortIndex: number;
}

3.2 最小堆实现任务队列(保证取到最高优先级)

代码语言:javascript
复制
class MinHeap {
  heap: Task[] = [];
  push(node: Task) { /* 上浮调整 */ }
  pop(): Task | null { /* 下沉调整 */ }
  peek(): Task | null { return this.heap[0] || null; }
}

3.3 核心调度循环(workLoop

代码语言:javascript
复制
let scheduledHostCallback: ((hasTimeRemaining: boolean, initialTime: number) => boolean) | null = null;
let isMessageLoopRunning = false;

// 使用MessageChannel模拟宏任务
const channel = new MessageChannel();
const port = port2;

channel.port1.onmessage = function () {
  if (scheduledHostCallback !== null) {
    const currentTime = performance.now();
    // 检查是否还有剩余时间(5ms切片)
    const hasTimeRemaining = true; 
    let didTimeout = false;
    // 执行任务循环
    const continuation = scheduledHostCallback(hasTimeRemaining, currentTime);
    if (continuation) {
      port.postMessage(null); // 继续下一帧切片
    } else {
      scheduledHostCallback = null;
    }
  }
};

export function scheduleCallback(priorityLevel: number, callback: () => void) {
  const currentTime = performance.now();
  const timeout = getTimeoutByPriority(priorityLevel);
  const expirationTime = currentTime + timeout;
  const task: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime: currentTime,
    expirationTime,
    sortIndex: expirationTime // 按过期时间排序
  };
  taskQueue.push(task);
  // 如果当前无调度,触发MessageChannel
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
  return task;
}

export function shouldYield() {
  // 检查当前帧剩余时间是否小于1ms
  return getCurrentTime() >= frameDeadline;
}

逼真源码细节:高优先级任务(如Immediate)不会走MessageChannel,而是直接同步执行(performSyncWorkOnRoot)。普通更新走上述performConcurrentWorkOnRoot


4. 协调器(Reconciler)—— beginWork 与 completeWork

协调器负责遍历Fiber树,生成workInProgress。我们将实现最核心的performUnitOfWork

4.1 工作循环(支持中断)

代码语言:javascript
复制
function workLoopConcurrent() {
  while (nextUnitOfWork !== null && !shouldYield()) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
}

function performUnitOfWork(unit: Fiber): Fiber | null {
  // 1. beginWork:处理当前节点,生成子Fiber
  const next = beginWork(unit);
  if (next === null) {
    // 2. completeWork:没有子节点则向上完成
    return completeUnitOfWork(unit);
  }
  return next; // 返回子节点,继续深度遍历
}

4.2 beginWork 核心逻辑(处理函数组件与宿主组件)

代码语言:javascript
复制
function beginWork(current: Fiber | null, workInProgress: Fiber): Fiber | null {
  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type;
      const resolvedProps = workInProgress.pendingProps;
      // 关键:执行函数组件,返回React元素(JSX结构)
      // 此处注入Hooks上下文
      renderWithHooks(current, workInProgress, Component, resolvedProps);
      const children = workInProgress.memoizedState?.element ?? null;
      // 调和子节点(Diff)
      reconcileChildren(current, workInProgress, children);
      return workInProgress.child;
    }
    case HostComponent: {
      // DOM标签节点(如div)
      const type = workInProgress.type;
      const props = workInProgress.pendingProps;
      // 此时不创建DOM,延迟到completeWork
      const children = props.children;
      reconcileChildren(current, workInProgress, children);
      return workInProgress.child;
    }
    case HostRoot: {
      // 根节点
      const children = workInProgress.memoizedState?.element ?? null;
      reconcileChildren(current, workInProgress, children);
      return workInProgress.child;
    }
    default: return null;
  }
}

4.3 单节点与多节点Diff(简化版)

真实的React Diff分单节点和多节点(数组)。核心逻辑:复用节点标记删除

代码语言:javascript
复制
function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any) {
  if (current === null) {
    // 首次渲染:所有子节点均标记为 Placement
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
  } else {
    // 更新:复用或更新
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren);
  }
}

// 针对单节点(非数组)的复用判断
function reconcileSingleElement(returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) {
    if (child.key === key && child.type === element.type) {
      // 节点可复用 -> 克隆并打上Update Flag
      const existing = useFiber(child, element.props);
      existing.return = returnFiber;
      // 删除剩余兄弟节点
      deleteRemainingChildren(returnFiber, child.sibling);
      return existing;
    }
    child = child.sibling;
  }
  // 新建节点
  const created = createFiberFromElement(element);
  created.return = returnFiber;
  return created;
}

4.4 completeWork 与 DOM 实例化

遍历完子节点后,执行completeWork,创建真实DOM(渲染器层)并记录副作用(effectList)。

代码语言:javascript
复制
function completeUnitOfWork(unit: Fiber): Fiber | null {
  const completedWork = unit;
  const current = completedWork.alternate;
  switch (completedWork.tag) {
    case HostComponent: {
      if (current !== null && completedWork.stateNode !== null) {
        // 更新节点(属性变更)
        updateHostComponent(current, completedWork);
      } else {
        // 创建DOM实例
        const instance = createInstance(completedWork.type, completedWork.pendingProps);
        // 将子DOM节点追加到当前节点
        appendAllChildren(instance, completedWork);
        completedWork.stateNode = instance;
      }
      break;
    }
    case HostText: {
      const newText = completedWork.pendingProps;
      const instance = createTextInstance(newText);
      completedWork.stateNode = instance;
      break;
    }
  }
  // 收集副作用形成effectList(链表)
  const sibling = completedWork.sibling;
  if (sibling !== null) {
    return sibling; // 返回兄弟节点,继续工作
  }
  // 否则向上返回父级
  return completedWork.return;
}

5. 提交阶段(Commit)—— 三阶段分离

为了处理useEffectuseLayoutEffect,React将提交拆分为before mutationmutationlayout三大阶段。

代码语言:javascript
复制
function commitRoot(root: FiberRoot) {
  const finishedWork = root.finishedWork;
  const firstEffect = finishedWork.firstEffect;
  
  // 阶段1: before mutation(获取DOM快照,仅用于类组件getSnapshotBeforeUpdate)
  commitBeforeMutationEffects(firstEffect);
  
  // 阶段2: mutation(增删改DOM,执行useLayoutEffect的销毁函数)
  commitMutationEffects(firstEffect);
  root.current = finishedWork; // 切换current指针!此时新树生效
  
  // 阶段3: layout(执行useLayoutEffect回调,调度useEffect)
  commitLayoutEffects(firstEffect);
}

function commitMutationEffects(effect: Fiber | null) {
  while (effect !== null) {
    if (effect.flags & Placement) {
      // 插入DOM
      commitPlacement(effect);
      effect.flags &= ~Placement;
    }
    if (effect.flags & Update) {
      // 更新DOM属性
      commitUpdate(effect);
      effect.flags &= ~Update;
    }
    if (effect.flags & Deletion) {
      // 删除DOM并执行所有Effect的清理
      commitDeletion(effect);
    }
    effect = effect.nextEffect;
  }
}

6. Hooks 在 Fiber 上的真·实现原理

React18 Hooks最大的秘密:Hooks链表挂载在Fiber节点的memoizedState,且调用顺序依赖全局索引。

6.1 全局上下文与Hook结构

代码语言:javascript
复制
let currentlyRenderingFiber: Fiber | null = null;
let workInProgressHook: Hook | null = null;
let currentHook: Hook | null = null;
let hookIndex = 0;

interface Hook {
  memoizedState: any;      // 当前值
  baseState: any;          // 基础状态
  baseQueue: Update<any, any> | null;
  queue: UpdateQueue<any, any> | null; // 更新环状链表
  next: Hook | null;
}

// 更新环状链表结构
interface Update<State> {
  lane: Lane;
  action: (state: State) => State;
  next: Update<State> | null;
}

6.2 useState 源码级实现

代码语言:javascript
复制
function useState<S>(initialState: S): [S, (action: (S) => S) => void] {
  // 1. 获取当前Hook(首次mount或更新)
  const hook = mountState(initialState) if (currentlyRenderingFiber.alternate === null) else updateState(initialState);
  
  // 2. 计算最新状态(遍历baseQueue)
  let state = hook.memoizedState;
  const queue = hook.queue;
  if (queue !== null) {
    let update = queue.first;
    while (update !== null) {
      state = update.action(state);
      update = update.next;
    }
    hook.memoizedState = state;
  }
  
  // 3. 返回dispatch函数(绑定fiber和queue)
  const dispatch = createDispatch(queue);
  return [state, dispatch];
}

// 创建更新(触发调度)
function createDispatch(queue: UpdateQueue<any>): any {
  return function (action: any) {
    const update = { lane: DefaultLane, action, next: null };
    // 将update加入环状链表
    if (queue.first === null) {
      update.next = update;
      queue.first = update;
    } else {
      const last = queue.first.next;
      queue.first.next = update;
      update.next = last;
    }
    // 触发根节点调度 -> scheduleUpdateOnFiber
    scheduleUpdateOnFiber(currentlyRenderingFiber);
  };
}

6.3 useEffect 的调度原理

useEffect不直接操作DOM,而是将副作用放入effectList,在layout阶段异步执行(通过SchedulerNormalPriority)。

代码语言:javascript
复制
function useEffect(create: () => (() => void) | void, deps: any[] | void) {
  const hook = mountWorkInProgressHook(); // 或 updateWorkInProgressHook
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = {
    tag: HookHasEffect | (hook.memoizedState?.effect?.tag ?? 0),
    create,
    destroy: undefined,
    deps: nextDeps,
    next: null
  };
  hook.memoizedState = effect;
  // 将effect挂载到fiber的updateQueue上,供commit阶段处理
  pushEffect(HookHasEffect, create, undefined, nextDeps, currentlyRenderingFiber);
}

7. Lane并发模型与任务中断恢复

React18最迷人的地方在于高优先级任务打断低优先级任务

7.1 Lane定义(31位二进制)

代码语言:javascript
复制
export const TotalLanes = 31;
export const SyncLane = 0b0000000000000000000000000000001; // 1
export const InputContinuousLane = 0b0000000000000000000000000000100; // 4
export const DefaultLane = 0b0000000000000000000000000010000; // 16
export const IdleLane = 0b0100000000000000000000000000000; // 2^30

7.2 中断恢复机制(throwException模拟)

当高优先级更新(如SyncLane)到达时,ensureRootIsScheduled会重新调度。正在进行的工作单元会被抛弃(workInProgress树丢弃),并复用current树从头开始。

代码语言:javascript
复制
function markRootUpdated(root: FiberRoot, lane: Lane) {
  root.pendingLanes |= lane;
  if (lane !== SyncLane) {
    // 如果已有低优先级任务,中断它
    if (root.callbackNode !== null) {
      cancelCallback(root.callbackNode);
      root.callbackNode = null;
    }
    // 用新的优先级调度
    ensureRootIsScheduled(root);
  }
}

function ensureRootIsScheduled(root: FiberRoot) {
  const nextLanes = getNextLanes(root);
  // 根据最高优先级选择同步或并发执行
  if (nextLanes === SyncLane) {
    scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    const priority = lanesToSchedulerPriority(nextLanes);
    scheduleCallback(priority, performConcurrentWorkOnRoot.bind(null, root));
  }
}

8. 串联:createRoot 启动函数

最后,将上述所有模块串联成一个可运行的ReactDOM.createRoot

代码语言:javascript
复制
export function createRoot(container: HTMLElement): ReactDOMRoot {
  const root = createFiberRoot(container);
  return {
    render: (children: ReactElement) => {
      const update = createUpdate(DefaultLane);
      update.payload = { element: children };
      enqueueUpdate(root.current, update);
      scheduleUpdateOnFiber(root.current, DefaultLane);
    }
  };
}

// 入口调度
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
  const root = getRootForUpdatedFiber(fiber);
  markRootUpdated(root, lane);
  ensureRootIsScheduled(root);
}

9. 总结:React18 的“道”

通过手写这300行核心逻辑,我们可以清晰透视React18的本质:

  • Fiber链表解决的是“暂停与恢复”的数据结构问题。
  • MessageChannel + 任务堆解决的是“何时让出主线程”的调度问题。
  • Lane二进制解决的是“谁更紧急”的优先级问题。
  • 双缓冲 + 副作用链表解决的是“安全且高效地更新DOM”的渲染问题。

这便是卡颂所推崇的“源码向”学习法——抛开晦涩的术语,直击最小的可工作原语。当你亲手构建出这个迷你React,再看官方源码中的ReactFiberWorkLoop,便会发现万物同源,大道至简。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 【卡颂】从0实现React18:手写并发调度引擎与Fiber架构
    • 1. 破局:为什么React18必须从Fiber开始?
    • 2. 基石:定义Fiber节点与全局状态
    • 3. 调度器(Scheduler)—— 时间切片与优先级队列
      • 3.1 优先级定义与任务结构
      • 3.2 最小堆实现任务队列(保证取到最高优先级)
      • 3.3 核心调度循环(workLoop)
    • 4. 协调器(Reconciler)—— beginWork 与 completeWork
      • 4.1 工作循环(支持中断)
      • 4.2 beginWork 核心逻辑(处理函数组件与宿主组件)
      • 4.3 单节点与多节点Diff(简化版)
      • 4.4 completeWork 与 DOM 实例化
    • 5. 提交阶段(Commit)—— 三阶段分离
    • 6. Hooks 在 Fiber 上的真·实现原理
      • 6.1 全局上下文与Hook结构
      • 6.2 useState 源码级实现
      • 6.3 useEffect 的调度原理
    • 7. Lane并发模型与任务中断恢复
      • 7.1 Lane定义(31位二进制)
      • 7.2 中断恢复机制(throwException模拟)
    • 8. 串联:createRoot 启动函数
    • 9. 总结:React18 的“道”
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档