首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《从0实现React18:只写核心逻辑,不抄源码》

《从0实现React18:只写核心逻辑,不抄源码》

原创
作者头像
用户12689597
发布2026-08-19 11:42:47
发布2026-08-19 11:42:47
170
举报

别被"虚拟DOM""Diff算法"唬住,React18最本质的就三件事:打断、优先级、链表


一、先忘掉JSX,回到"CreateElement"

React18无论多花哨,入口依然是React.createElement。我们自己实现一个极简版:

代码语言:javascript
复制
// 仅核心逻辑,去掉大量边界处理
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.flat().filter(child => child != null)
    }
  };
}

工程要点

  • children扁平化处理(防止嵌套数组)
  • 过滤null/undefined(这是React18的硬性要求)
  • 返回的是普通对象,不是真实DOM,也不是虚拟DOM(它只是Fiber节点的素材

二、核心变革:从"递归"到"循环链表"(Fiber架构)

React15用递归更新,一旦开始就无法中断。React18用Fiber链表,实现了可打断的更新

2.1 Fiber节点的数据结构(仅6个关键字段)

代码语言:javascript
复制
// 每个Fiber节点只保留最核心的指针
function createFiberNode(element) {
  return {
    type: element.type,           // 组件类型
    props: element.props,         // 属性
    dom: null,                    // 对应的真实DOM
    child: null,                  // 第一个子Fiber
    sibling: null,                // 下一个兄弟Fiber
    return: null,                 // 父Fiber(用于回溯)
    alternate: null               // 双缓冲的"替身"节点
  };
}

为什么这样设计?

  • child/sibling/return构成单向链表,方便深度优先遍历时随时暂停/恢复
  • alternate是React18双缓冲机制的关键:内存中有两棵树,一棵当前显示,一棵正在构建

2.2 工作循环:让渲染"可打断"(核心5行)

React18的渲染引擎不再是一次性while循环,而是配合浏览器的requestIdleCallback(生产环境用Scheduler):

代码语言:javascript
复制
let nextUnitOfWork = null;  // 下一个待处理的Fiber节点

function workLoop(deadline) {
  // 有空闲时间 && 还有任务未完成
  while (nextUnitOfWork && deadline.timeRemaining() > 1) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  // 没完成就继续请求下一帧
  if (nextUnitOfWork) {
    requestIdleCallback(workLoop);
  }
}

这就是React18并发的基础:每帧只干一点活,不阻塞浏览器绘制。用户点击按钮时,高优先级任务可以插队


三、Hooks的本质:不是魔法,是"链表+闭包"

React18的Hooks之所以能在函数组件中使用,是因为Fiber节点上挂载了一个Hook链表

3.1 Hook在Fiber上的存储结构

代码语言:javascript
复制
// 每个Fiber节点新增两个字段
fiber.memoizedState = null;   // 指向第一个Hook
fiber.updateQueue = null;     // 存放副作用(useEffect)

// 单个Hook的数据结构(以useState为例)
hook = {
  queue: [],                  // 更新队列
  memoizedState: initialState // 当前状态
  next: null                  // 指向下一个Hook
};

3.2 极简版useState实现(仅10行核心逻辑)

代码语言:javascript
复制
let currentFiber = null;
let hookIndex = 0;

function useState(initialValue) {
  const fiber = currentFiber;
  const hooks = fiber.memoizedState || [];
  const currentHook = hooks[hookIndex] || { queue: [], memoizedState: initialValue };
  
  // 执行所有更新(类似setState的批处理)
  currentHook.queue.forEach(action => {
    currentHook.memoizedState = action(currentHook.memoizedState);
  });
  currentHook.queue = [];
  
  // 保存并推进索引
  hooks[hookIndex] = currentHook;
  fiber.memoizedState = hooks;
  hookIndex++;
  
  // 返回状态 + 派发函数(闭包捕获当前Hook)
  return [
    currentHook.memoizedState,
    (action) => {
      currentHook.queue.push(action);
      scheduleUpdate(fiber);  // 触发重新渲染
    }
  ];
}

关键理解

  • Hook顺序由hookIndex决定,所以不能在条件语句中使用Hooks
  • scheduleUpdate不直接更新DOM,而是把该Fiber标记为"需要更新",放入调度队列
  • 每次渲染都会重新执行整个函数组件,但Hook链表保证了状态不丢失

四、并发渲染的"优先级抢占"(最小实现)

React18的startTransition本质是给更新打标记

代码语言:javascript
复制
let currentPriority = 0;          // 0最高,数字越大优先级越低
const URGENT_PRIORITY = 0;
const TRANSITION_PRIORITY = 1;

function startTransition(callback) {
  const prevPriority = currentPriority;
  currentPriority = TRANSITION_PRIORITY;
  callback();
  currentPriority = prevPriority;
}

// 调度时判断优先级
function scheduleUpdate(fiber) {
  const priority = currentPriority;
  if (priority === URGENT_PRIORITY) {
    // 立即执行,不经过workLoop
    performUnitOfWork(fiber);
  } else {
    // 放入队列,等空闲时间
    nextUnitOfWork = fiber;
    requestIdleCallback(workLoop);
  }
}

就是这么简单:通过一个全局变量控制"紧急程度",紧急更新直接同步执行,过渡更新让出主线程。


五、从0到React18:你缺少的"最后1公里"

上面三块(Fiber链表 + 可打断循环 + Hook闭包)加起来不到50行核心逻辑,但已经涵盖了React18最本质的设计哲学。你可能会说:"这连Diff都没有,算哪门子React?"

真相是:React18的Diff算法(reconcileChildren)只是Fiber构建过程中的一个子函数,它的复杂度被严重高估了。真正让React18成为"React18"的,是调度器(Scheduler)双缓冲树的组合。

如果你真想从0实现一个可运行的最小React18,还缺这两块(但每一块都可以用10行代码模拟):

  1. 渲染器:把Fiber树转成真实DOM(递归挂载)
  2. 更新触发器setState时标记根节点,重新启动workLoop

六、工程师的顿悟时刻

React18不是"写出来的",是"设计出来的"。

  • Fiber解决的是"如何停下"(数据结构问题)
  • Scheduler解决的是"何时继续"(调度问题)
  • Hook解决的是"状态放哪里"(存储问题)

这三个问题各用不到20行核心代码就能讲清楚,但它们在源码中被上万行边界逻辑包裹。当你理解了这50行,你就拥有了造轮子的骨架,剩下的全是工程细节。

下次再看到ReactDOM.createRoot,你脑子里应该浮现的是:

  • 创建一个Fiber根节点
  • 启动workLoop循环
  • 等待setState触发更新入队

最后留一道工程师的思考题: 如果让你用这50行核心逻辑去实现useMemo,你会如何在Hook链表上挂载"缓存值"? (提示:只需要在Hook对象上加一个deps数组,每次渲染对比即可。)

答案在你动手写第51行代码的时候,自然浮现。

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

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

目录
  • 一、先忘掉JSX,回到"CreateElement"
  • 二、核心变革:从"递归"到"循环链表"(Fiber架构)
    • 2.1 Fiber节点的数据结构(仅6个关键字段)
    • 2.2 工作循环:让渲染"可打断"(核心5行)
  • 三、Hooks的本质:不是魔法,是"链表+闭包"
    • 3.1 Hook在Fiber上的存储结构
    • 3.2 极简版useState实现(仅10行核心逻辑)
  • 四、并发渲染的"优先级抢占"(最小实现)
  • 五、从0到React18:你缺少的"最后1公里"
  • 六、工程师的顿悟时刻
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档