前端识记mini-vue-reading
00-总览与阅读路线
第 0 章 · 总览与阅读路线
最后更新 2026年8月29日
第 0 章 · 总览与阅读路线
在打开任何一个源码文件之前,必须先回答三个问题:这个项目由哪几块组成?块与块之间谁依赖谁?我该从哪一块进去? 这一章就干这一件事。
0.1 mini-vue 的包结构
mini-vue 复刻了 Vue 3 的 monorepo 结构(Vue 3 在 packages/ 下有 20+ 个包,mini-vue 保留了最核心的 5 个):
mini-vue/src
├── shared/ # 公共工具:isObject、hasChanged、ShapeFlags、camelize...
├── reactivity/ # 响应式系统:reactive、ref、effect、computed
├── runtime-core/ # 平台无关的运行时:vnode、组件、patch、diff
├── runtime-dom/ # 浏览器平台实现:真正操作 DOM 的 5 个函数
└── compiler-core/ # 编译器:template 字符串 → render 函数源码每个包各自有 index.ts 做统一导出,有自己的 __tests__/ 目录。测试目录是本剖析反复引用的对象——它是每段代码的"存在理由清单"。
0.2 三大系统怎么协作:跑一遍完整链路
拿一个最小的 Vue 应用,把三大系统串起来走一遍。这个心智模型贯穿全部章节:
<div>{{ message }}</div>createApp({
setup() {
const message = ref('hello')
return { message }
}
}).mount('#app')发生的事情按时间顺序:
【编译时 · compiler】
"<div>{{ message }}</div>"
→ parse → AST(树形对象,描述"一个 div,里面是插值 message")
→ transform → 加工 AST(标记哪些是动态的、准备好生成代码需要的信息)
→ codegen → "function render(_ctx) { return h('div', null, _ctx.message) }"
【运行时首次挂载 · runtime + reactivity】
mount('#app')
→ 创建组件实例 instance
→ 执行 setup(),拿到 { message }(ref 是 reactivity 提供的)
→ 把 render 函数包进 effect 里执行 ← ★ 两大系统在这里接线
→ render 执行,读了 message.value
→ reactivity 记下:"这个组件的渲染依赖 message"(track)
→ render 返回 vnode 树
→ patch(null, vnode):递归创建真实 DOM,插入 #app
【数据变化 · reactivity 驱动 runtime】
message.value = 'world'
→ reactivity 发现 message 被写(trigger)
→ 查表:"谁依赖 message?"→ 那个组件的渲染 effect
→ 重新执行 effect → render 再跑一遍 → 新 vnode 树
→ patch(旧vnode, 新vnode):对比两棵树,只更新变了的文本一句话总结三者分工:
compiler 负责把模板翻译成 render 函数(只做一次); runtime 负责执行 render 函数、把 vnode 变成真实 DOM、以及新旧 vnode 的对比更新; reactivity 负责回答"数据变了,谁需要重新执行"。
0.3 依赖方向决定阅读顺序
画出包依赖图(箭头 = 依赖):
runtime-dom ──→ runtime-core ──→ reactivity ──→ shared
↑
compiler-core ───────┘(产物 render 函数调用 runtime 的 h/createElementVNode)三个观察:
- reactivity 除了 shared 之外零依赖。它不知道组件、不知道 vnode、不知道 DOM 的存在,是一个可以单独发包使用的库(Vue 官方确实单独发布了
@vue/reactivity)。零依赖 = 不需要任何前置知识就能读 = 最佳入口。 - runtime-core 不依赖 runtime-dom,反而是被依赖。这是故意倒转的:核心运行时通过"参数注入"拿到平台操作函数,而不是直接 import DOM API。第 3 章细讲。
- compiler 和 runtime 几乎不相互 import,它们通过一个"约定的产物格式"(render 函数长什么样)解耦。这就是为什么 Vue 可以只用 runtime(手写 render 函数 / 构建时预编译),把 compiler 从生产包里整个剔除。
所以阅读顺序是确定的,不是品味问题:
reactivity(1-2 章) → runtime 挂载(3-4 章) → runtime 更新(5 章) → compiler(6 章)0.4 每一章的读法约定
后面每章遵循同一个结构,这个结构本身就是"源码是怎么写出来的"的还原:
- 问题:这个文件/函数要解决什么,不解决会怎样;
- 最小失败用例:mini-vue 的作者是 TDD 式开发的,每个特性先有一个红的测试。我们把这个测试摆出来——它是这段代码唯一的需求文档;
- 代码:完整实现,不省略;
- 逐段解释:按代码块顺序讲每段为什么这么写、换种写法会死在哪个用例上;
- 陷阱与细节:容易读漏、面试常问、或真源码里处理得更复杂的点。
0.5 开始前的准备(10 分钟)
git clone https://github.com/cuixiaorui/mini-vue.git
cd mini-vue
pnpm install
pnpm test # 全部测试跑通,绿色,心里有底再执行一条命令,它的输出就是官方阅读顺序:
git log --oneline --reversemini-vue 是按"一个功能一个 commit"开发的,最早的 commit 就是 feat: effect & reactive——和本剖析的章节顺序一致。读某段代码困惑时,git log -p -- src/reactivity/effect.ts 看它的演化史,比盯着最终形态猜要快得多。
自测
进入第 1 章前,不看上文回答:
- 三大系统各自的一句话职责?
- 为什么 reactivity 是最佳阅读入口?(两个理由)
- runtime-core 为什么不 import runtime-dom,反而被它依赖?