被技术迭代追着跑的日子
前端圈的节奏越来越快。
Vue 3 刚稳定没多久,React Server Components 开始铺开;Next.js 版本号一路狂飙,Remix 紧追不舍;Solid 开始崭露头角,Qwik 说自己是下一代;Svelte 5 的 Runes 语法又来了...
打开技术社区,满眼都是新框架、新工具、新范式。今天学的明天可能就过时了,上周还在讨论的最佳实践,下周就被推翻。
更让人疲惫的是生态链上的"套娃":用 Vue 就得学 Pinia + VueUse + Element Plus + VitePress;用 React 就要选 Redux/Zustand + React Query + Ant Design + Umi... 每一层都有自己的学习成本和坑。
我不是在写代码,我是在背文档。
一个"离经叛道"的决定
有一天我停下来问了自己一个问题:
如果我把花在学习这些框架上的时间,用来从零写一个自己的框架,会怎样?
这个想法一冒出来,我自己都觉得荒谬。网上到处都是这样的声音:
- "别造轮子了,Vue/React 已经够好了"
- "你写的能比得上团队维护了几年的项目吗?"
- "重复造轮子是没有价值的"
- "有这时间不如多学几个库"
这些话我都听过,甚至我自己也说过类似的话。但这一次,我想换个活法。
与其一辈子追着别人的车跑,不如自己造一辆能开的。
从虚拟 DOM 到信号驱动:我的技术探索之路
第一阶段:踩在前人的肩膀上
我没有从零发明一种全新的范式——那不现实。我参考了各种框架的公开文档和社区讨论,了解它们的设计思路和解决的问题,然后用自己的方式去实现。
虚拟 DOM 为什么流行?
它解决了一个真实问题:直接操作 DOM 太慢了,而且跨平台困难。用 JavaScript 对象描述 UI 结构,然后批量更新,这是一个优雅的抽象。
但我很快发现了一个问题:Diff 算法的开销是不可避免的。
// 这是一个很常见的场景
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
当 todos 数组中某一项的 text 变化时,整个组件会重新执行,生成新的虚拟 DOM 树,然后 Diff 算法逐个比对 100 个 <li> 节点,最后发现只有 1 个变了。
明明只改了一个字,为什么要检查剩下 99 个?
第二阶段:从零摸索响应式系统
说实话,我并没有深入研究过 Vue 的源码——我看过的 Vue 源码加起来不超过 100 行。Vitarx 响应式系统的设计,更多是我在多次尝试、反复重构、与 AI 讨论方案的过程中逐步打磨出来的。
一开始我用的是最朴素的方式:
// 第一版:用 Set 存储依赖
const deps = new Set<Function>()
function ref(value) {
return new Proxy({ value }, {
get() {
// 谁在读?记下来
if (activeEffect) deps.add(activeEffect)
return value
},
set(_, newValue) {
value = newValue
// 通知所有依赖者
deps.forEach(fn => fn())
return true
}
})
}
能用,但问题很多:依赖无法精确清理、重复触发、内存泄漏... 前后重构了不下十次,最终演变成了现在的双向链表依赖管理结构。这是 Vitarx 响应式引擎(@vitarx/responsive)的实际实现:
核心数据结构:DepLink 双向链表节点
/**
* DepLink — 信号与副作用之间的双向链表节点
*
* 每个节点同时存在于两条链表中:
* - Signal 链表:该信号被哪些 effect 依赖
* - Effect 链表:该 effect 依赖了哪些信号
*/
class DepLink {
// ── Signal 维度(该信号的所有订阅者)──
sigPrev?: DepLink // Signal 链表中的前驱
sigNext?: DepLink // Signal 链表中的后继
// ── Effect 维度(该 effect 的所有依赖)──
ePrev?: DepLink // Effect 链表中的前驱
eNext?: DepLink // Effect 链表中的后继
constructor(public signal: Signal, public effect: EffectRunner) {}
}
这个设计的精妙之处在于:通过一个节点同时维护两个维度的链表,实现了 O(1) 的依赖建立和 O(n) 的精确触发。
Track(依赖收集):建立链表关联
当副作用函数执行并访问响应式数据时:
export function trackSignal(signal: Signal): void {
const activeEffect = currentActiveEffect // 当前正在执行的 effect
if (!activeEffect) return // 没有活跃 effect,不追踪
// 创建或复用链表节点,挂到两条链表的尾部
let link = activeEffect[DEP_INDEX_MAP]?.get(signal)
if (!link) {
link = createDepLink(activeEffect, signal)
activeEffect[DEP_INDEX_MAP]?.set(signal, link)
}
link[DEP_VERSION] = activeEffect[DEP_VERSION] // 标记为"本次仍访问"
}
// createDepLink 内部:
// 1. 挂到 signal 的 dep 链表尾部(signal → sigPrev ↔ link ↔ sigNext)
// 2. 挂到 effect 的 dep 链表尾部(effect → ePrev ↔ link ↔ eNext)
执行过程可视化:
执行 effect(() => { count.value + name.value })
步骤1: 访问 count → trackSignal(count)
count 链表: HEAD ←→ [linkA] ←→ TAIL (新增 linkA)
effect链表: HEAD ←→ [linkA] ←→ TAIL
步骤2: 访问 name → trackSignal(name)
name 链表: HEAD ←→ [linkB] ←→ TAIL (新增 linkB)
effect链表: HEAD ←→ [linkA] ←→ [linkB] ←→ TAIL
Trigger(触发更新):沿链表精确通知
当数据变化时,只需遍历 Signal 链表即可通知所有依赖者——不需要 Diff,不需要重渲染:
export function triggerSignal(signal: Signal): void {
// 从 Signal 链表头部开始,逐个通知依赖它的 effect
for (let link = signal[SIGNAL_DEP_HEAD]; link; ) {
const next = link.sigNext // 先保存下一个(effect 执行可能修改链表)
link.effect() // 直接调度 effect 执行
link = next // 移动到下一个
}
}
关于 API 命名:ref、reactive、computed、watch、effect 这些名字确实借鉴了 Vue 的风格——这不是因为 Vitarx 的实现和 Vue 相同,而是为了降低开发者的迁移成本和学习门槛。如果你熟悉 Vue,上手 Vitarx 几乎零成本。但底层实现完全是独立设计的。
第三阶段:Vitarx 的诞生
基于这些理解,我开始构建 Vitarx。核心理念可以概括为三点:
- 信号级精确更新 — 数据变化时,只更新关联的 DOM 节点
- 运行时视图树 — 保留完整的运行时结构,而非编译成静态代码
- 万物皆组件 — 所有内置组件都用公开 API 构建,无黑盒
import { ref } from 'vitarx'
function Counter() {
const count = ref(0)
return (
<div>
<p>Count: {count}</p>
{/* count 变化时,只有这个 <p> 的文本节点会被更新 */}
<button onClick={() => count.value++}>+1</button>
</div>
)
}
没有虚拟 DOM,没有 Diff,没有组件级重渲染。count 变化 → 沿双向链表定位到依赖它的文本节点 → 更新文本。三步完成。
"造轮子"给我带来了什么?
1. 对技术的真正掌控感
以前用框架时,遇到奇怪的行为我只能去搜 Issue、看文档、问社区。现在我遇到问题可以直接去看底层实现——因为那就是我自己写的。
案例:Teleport 传送门组件
在很多框架中,传送门(Portal)是底层运行时的特殊能力,开发者只能调用无法定制。但在 Vitarx 中,Teleport 就是一个普通函数组件,核心源码如下:
/**
* Teleport 组件 - 将子组件渲染到 DOM 树的其他位置
*
* 全部使用 vitarx 公开 API,没有任何黑盒操作
*/
function Teleport({ children, to, defer, disabled }) {
const child = useFastChild(children)
if (!child) return createCommentView('Teleport:empty')
if (disabled) return child
// 锚点占位符留在原位
const placeholder = createCommentView(`Teleport:${to}`)
const instance = getInstance()
onInit(() => { child.init(instance.subViewContext) })
const mount = () => {
const target = getTarget(to, instance.view.location)
if (target) child.mount(target) // 挂载到目标容器
}
defer ? onMounted(mount) : onBeforeMount(mount)
onShow(() => { if (child.isRuntime && !child.isActive) child.activate() })
onHide(() => { if (child.isRuntime && !child.isActive) child.deactivate() })
onDispose(() => child.dispose())
return placeholder
}
整个组件用到的全部是 Vitarx 公开 API:
| API | 来源包 | 用途 |
|---|---|---|
useFastChild | runtime-core | 获取并缓存子视图 |
createCommentView | runtime-core | 创建注释节点作为锚点 |
getInstance | runtime-core | 获取当前组件实例 |
onInit/onMounted/onBeforeMount/onDispose/onShow/onHide | runtime-core | 生命周期钩子 |
案例:自定义 Canvas 渲染器
万物皆组件的理念不仅限于内置组件的重写——你甚至可以替换整个渲染后端。Vitarx 通过 ViewRenderer 接口抽象了所有 DOM 操作,只需实现这个接口就能把 JSX 渲染到任何目标:
import { setRenderer, type ViewRenderer } from 'vitarx'
// 自定义 Canvas 渲染器 —— 实现 ViewRenderer 接口
const canvasRenderer: ViewRenderer = {
// ── 创建节点:用轻量对象代替 DOM 元素 ──
createElement(tag, parent) { /* 创建元素节点 */ },
createText(text) { /* 创建文本节点 */ },
createComment(text) { /* 创建注释节点(锚点占位)*/ },
createFragment(view) { /* 创建片段(For/Suspense 等容器)*/ },
// ── 类型判断 ──
isElement(node) { return !!node?.__canvas__ },
isSVGElement() { return false },
isMathMLElement() { return false },
isFragment(node) { return !!node?.__fragment__ },
// ── 节点操作 ──
append(child, parent) { /* 挂载子节点到父容器 */ },
insert(child, anchor) { /* 插入到锚点之前 */ },
replace(newNode, oldNode) { /* 替换旧节点 */ },
remove(node) { /* 从文档中移除节点 */ },
// ── 内容与属性 ──
setText(node, text) { /* 更新文本内容 */ },
setAttribute(el, key, next, prev){ /* 设置属性 / 绑定事件 */ }
}
// 注册后,所有 JSX 节点的创建、挂载、更新、销毁都走这个渲染器
setRenderer(canvasRenderer)
14 个方法,覆盖了框架与平台交互的全部能力。 实现它,你就能让 Vitarx 把 JSX 渲染到 Canvas、WebGL、终端 UI——任何你能想到的目标。
ViewRenderer 接口定义了 14 个方法,覆盖了创建、类型判断、结构操作、内容更新、属性修改等所有平台交互能力:
| 方法分类 | 方法 | 用途 |
|---|---|---|
| 创建 | createElement / createText / createComment / createFragment | 创建各类型的视图节点 |
| 类型判断 | isElement / isSVGElement / isMathMLElement / isFragment | 运行时节点类型识别 |
| 结构操作 | append / insert / replace / remove | 节点的增删改 |
| 内容 | setText | 文本内容更新 |
| 属性 | setAttribute | 属性设置与事件绑定 |
这意味着 Vitarx 的渲染层是完全可插拔的——DOM 只是默认选项,你可以渲染到 Canvas、WebGL、甚至终端 UI。
没有任何隐藏的特殊处理,没有编译器的魔法指令。
2. 不再被 API 变动绑架
React Hooks 的依赖数组规则变过好几次。每次升级都要检查 useEffect、useMemo、useCallback 的依赖是否正确。Vue 3 的 <script setup> 语法糖也经历过多轮调整。
但当你理解了底层的响应式原理后,这些 API 层面的变动不再是威胁。你知道它们在解决什么问题,也知道如果换一种方式该怎么实现。
3. 极高的可扩展性
Vitarx 的所有内置组件——For、Suspense、Lazy、Freeze、Transition、Teleport——全部是用公开 API 构建的普通组件。
这意味着你可以:
- 查看源码理解每一行是如何工作的
- 复制一份修改成符合你业务需求的版本
- 从头写一个同样能力的组件,因为你有相同的 API
- 换掉渲染器把 JSX 渲染到你想要的任何目标上
这不是理论上的可能性,而是设计层面的保证。
性能实测:数据说话
我不打算吹嘘 Vitarx 性能天下第一——那是骗人的。但客观的 Benchmark 数据说明它在高频更新场景下确实有优势。
以下数据来自 JS Framework Benchmark,数值越小越好(单位:毫秒):
高频更新场景(Vitarx 的主战场)
部分更新(每10行修改1行)
Vue 3.6 ████████████████████ 19.1ms
React 19 ████████████ 11.2ms
Vitarx ██████████ 9.2ms ✅ 最快
选中行高亮(点击一行变色)
Vue 3.6 ██████████ 9.8ms
React 19 ███████ 5.8ms
Vitarx ████ 4.5ms ✅ 最快
交换两行位置(拖拽排序)
Vue 3.6 ██████████ 10.7ms 🥇 并列最优
React 19 ████████████████████████████████████████████ 67.6ms
Vitarx ██████████ 10.4ms 🥇 并列最优
综合指标对比
| 测试场景 | Vue 3.6 | React 19 | Vitarx 4.0 | 结论 |
|---|---|---|---|---|
| 部分更新(每10行) | 19.1ms | 11.2ms | 9.2ms | Vitarx 最快 |
| 选中行高亮 | 9.8ms | 5.8ms | 4.5ms | Vitarx 最快 |
| 交换两行 | 10.7ms | 67.6ms | 10.4ms | Vitarx ≈ Vue |
| 删除一行 | 20.9ms | 18.1ms | 17.2ms | Vitarx 最快 |
| 包体积(压缩) | 22.9KB | 51.4KB | 17.0KB | Vitarx 最小 |
| 首次绘制 | 134.4ms | 359.6ms | 137.7ms | Vitarx 接近原生 |
| 创建1000行 | 81.9ms | 82.4ms | 112.2ms | Vue/Vue 最快 |
| 创建10000行 | 880.2ms | 1143.5ms | 1104.1ms | Vue 最快 |
结论很清晰:在高频状态更新场景下(选中、交换、部分修改),Vitarx 的细粒度更新策略优势明显。而在大规模静态创建场景下,成熟框架的优化更到位。
性能很重要,但它不是唯一重要的东西。架构的可理解性、可扩展性、可控性,同样是衡量一个框架价值的关键维度。
回应质疑:"造轮子"到底有没有价值?
质疑一:"你写的能比得上 Vue/React 吗?"
坦白说,在很多方面确实比不上。Vue 有庞大的团队和多年的打磨,React 有 Meta 的工程体系支撑。Vitarx 在成熟度、生态丰富度、工具链完善程度上都有差距。
但这不是重点。
重点是:通过亲手实现,我对前端框架的理解达到了一个以前无法企及的高度。这种理解力是通用的——即使以后我用回 Vue 或 React,我也是带着"知其所以然"的心态在使用,而不是照着文档抄代码。
质疑二:"重复造轮子是浪费时间"
如果你只是复制粘贴一份已有的东西,那确实是浪费时间。但如果你的目标是深入理解,那么"造轮子"可能是最高效的学习方式。
我在这个过程中学到的东西包括但不限于:
- 响应式系统的双向链表依赖管理 — track/trigger 的链表操作、版本号增量清理
- 视图树的设计与生命周期管理 — init/mount/update/dispose 完整流转
- JSX 转换原理与自定义 JSX 运行时 — 不依赖官方 jsx-runtime
- SSR 的流式渲染与客户端水合 — renderToString / renderToStream
- 插件系统的设计模式 — App.use() 的链式调用与依赖注入
- TypeScript 类型体操的实战应用 — 条件类型、模板字面量、infer
这些东西,靠读文档是学不到这么深的。
质疑三:"没人会用你自己写的框架"
这话有一定道理。让开发者切换框架是一个很高的门槛,需要足够的理由。
但 Vitarx 不是为了让所有人放弃 Vue/React 而存在的。它是为那些有特殊需求的人准备的:
- 需要深度定制框架行为的项目
- 对性能有极致要求的场景
- 想要学习和研究框架原理的开发者
- 追求架构透明度和可控性的团队
只要能帮助到这些人,这个框架就有存在的意义。
Vitarx 技术架构一览
┌─────────────────────────────────────────────┐
│ vitarx │ ← 聚合包
├────────────┬─────────────┬──────────────────┤
│runtime-dom │ runtime-ssr │ │ ← 平台适配层
├────────────┴─────────────┼──────────────────┤
│ runtime-core │ utils │ ← 运行时核心
├──────────────────────────┼──────────────────┤
│ responsive │ │ ← 响应式系统
└──────────────────────────┴──────────────────┘
| 包名 | 职责 | 核心 API |
|---|---|---|
@vitarx/responsive | 响应式引擎(双向链表依赖管理) | ref, reactive, computed, watch, effect |
@vitarx/runtime-core | 组件与视图系统 | App, View, 生命周期, provide/inject, setRenderer |
@vitarx/runtime-dom | 浏览器渲染 | DOM 渲染器, Transition, Teleport, 指令系统 |
@vitarx/runtime-ssr | 服务端渲染 | renderToString, renderToStream |
快速体验
# 安装
npm install vitarx
# 或使用脚手架
pnpm create vitarx
import { ref, createApp } from 'vitarx'
function App() {
const count = ref(0)
return (
<div className="app">
<h1>Hello Vitarx</h1>
<p>Count: {count}</p>
<button onClick={() => count.value++}>+1</button>
<button onClick={() => count.value--}>-1</button>
</div>
)
}
createApp(App).mount('#app')
从零开始:给你的第一个迷你框架建议
如果你也被这篇文章触动了,想自己动手试试,这里有一条从易到难的实践路线:
第一步:实现一个最小响应式系统(1-2天)
这是所有现代前端框架的基石。你需要实现:
// 目标API
const count = ref(0)
effect(() => {
console.log('count is:', count.value) // 打印: count is: 0
})
count.value = 1 // 自动触发上面的 effect,打印: count is: 1
核心要点:
- 用 Proxy 或 Object.defineProperty 拦截读写
- 维护一个"当前活跃的 effect"全局变量
- 读时收集依赖(track),写时触发更新(trigger)
- 可以先用 Set 存储依赖,后续优化为双向链表
第二步:实现一个简易虚拟 DOM(2-3天)
// 目标API
const vdom = h('div', { class: 'app' },
h('p', null, 'Hello'),
h('button', { onClick: () => console.log('click') }, 'Click me')
)
render(vdom, document.getElementById('app')!)
核心要点:
- 实现
h()函数创建虚拟节点 - 实现
render()递归创建真实 DOM - 实现
patch()做简单的同层级 Diff - 处理属性更新和事件绑定
第三步:把响应式和视图结合起来(3-5天)
让 ref 的变化自动触发视图更新——恭喜你,你已经拥有了一个最原始的前端框架雏形!
后续方向(按兴趣选择)
- 加 JSX 支持:配置 Babel/TypeScript 的 jsx 运行时
- 加组件系统:函数组件 + 生命周期 + Props
- 加内置组件:For(列表)、If(条件)、Portal(传送门)
- 加 SSR 能力:renderToString 服务端渲染
- 换渲染目标:Canvas/WebGL/终端 UI
每完成一步,你对前端框架的理解就会深一层。 不需要一步到位,先跑起来再说。
写在最后
做 Vitarx 这两年,是我作为开发者成长最快的两年。
不是因为写了多少行代码,而是因为我终于跳出了"使用者"的思维局限,开始以"创造者"的视角看待技术。每一个设计决策背后的权衡,每一个 bug 背后的根因,每一种架构模式的优劣——这些都是被动使用框架时无法获得的体验。
如果你也厌倦了无休止地追逐技术潮流,如果你想真正理解手中的工具而不仅仅是使用它,不妨试试从零开始写点什么。
重要的是开始。
至于 Vitarx,它就在这里,开源、透明、欢迎审视。无论你是想用它来做项目,还是想通过阅读它的源码来学习框架原理,都欢迎:
造轮子的意义不在于轮子本身,而在于你在造的过程中所获得的一切。