
1. 为什么我要把 Vue3 源码拆成三条链路来读很多人第一次打开 Vue3 仓库会直接懵packages 目录下几十个包reactivity、runtime-core、runtime-dom、compiler-core、compiler-dom、compiler-sfc 各管一摊文档里又反复出现「响应式」「编译优化」「虚拟 DOM」这些词但真正落到代码上它们到底怎么串起来很少有人一次讲清楚。我自己的经验是别一上来就顺着文件树读而是先抓住三条主链路模板怎么被编译成渲染函数、响应式数据怎么被追踪、渲染函数执行时怎么触发更新。这三条链路分别对应 compiler、reactivity、runtime-core 三大模块把它们的协作关系理清Vue3 的整体架构就立住了。这篇内容适合已经会写 Vue3、但读源码时总在包与包之间迷路的人。我会先给出一套可复制的 VS Code 调试配置让你能直接在源码里打断点然后按「编译 → 响应式 → 渲染」的顺序逐模块验证调用链。整个过程不需要你背 API只需要跟着断点走一遍就能看到模板里的{{ time }}是怎么一步步变成真实 DOM 更新的。核心检索词就三个Vue3 源码分析、响应式系统、编译优化后面所有步骤都围绕它们展开。2. 读源码前先把调试环境搭好TaoToken 与 VS Code 配置读源码最怕两件事一是环境跑不起来二是断点打不中。我的做法是先用一个最小可运行的 Vue3 项目作为调试宿主再通过 sourcemap 把断点映射回源码。这里如果你需要调用模型来辅助理解某段编译产物可以用 TaoToken 的 API 做对话式追问它的接入地址是 https://taotoken.net/api 模型对话入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 适合边读代码边问「这段生成的 render 函数为什么长这样」。先准备调试宿主。用 Vite 建一个最小项目安装 Vue3 源码的本地链接版本这样断点才能进到 packages 里npm create vitelatest vue3-source-debug -- --template vue cd vue3-source-debug npm install # 把 vue 指向本地源码构建产物方便 sourcemap 映射 npm install vue3然后在项目根目录建.vscode/launch.json这是可复制的骨架重点是sourceMapPathOverrides把 webpack/vite 的路径映射回源码目录{ version: 0.2.0, configurations: [ { type: chrome, request: launch, name: Debug Vue3 Source, url: http://localhost:5173, webRoot: ${workspaceFolder}, sourceMaps: true, sourceMapPathOverrides: { webpack:///./node_modules/vue/*: ${workspaceFolder}/node_modules/vue/*, /fs/*: * }, breakOnLoad: true } ] }如果你要直接读 Vue3 仓库源码而不是 node_modules建议把仓库 clone 下来用 pnpm 构建出带 sourcemap 的 dev 版本再把宿主项目的vue别名指向packages/vue/dist/vue.global.js。这样断点会直接落在packages/reactivity/src/reactive.ts这类文件里而不是压缩后的产物。构建命令git clone https://github.com/vuejs/core.git vue3-core cd vue3-core pnpm install pnpm build vue -f global -d-d表示 dev 模式会保留 sourcemap。构建完成后在宿主项目的vite.config.js里加别名import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { vue: path.resolve(__dirname, ../vue3-core/packages/vue/dist/vue.global.js) } } })这一步做完你就能在 VS Code 里对reactive、effect、baseCreateRenderer这些函数直接下断点了。踩过的坑是如果 sourcemap 没生效断点会落在vue.global.js的 eval 代码里检查sourceMapPathOverrides的路径是否和实际构建输出一致即可。3. 响应式系统从 reactive 到 effect 的依赖收集链路响应式是 Vue3 源码里最独立、也最容易验证的一块。它位于packages/reactivity不依赖任何 DOM可以单独跑。核心就两个函数reactive创建代理effect注册副作用。先看reactive的实现骨架它用Proxy拦截 get/set并用两个 WeakMap 做缓存避免重复代理// packages/reactivity/src/reactive.ts 简化版 const proxyMap new WeakMap() const rawMap new WeakMap() export function reactive(target) { if (!isObject(target)) return target if (proxyMap.has(target)) return proxyMap.get(target) if (rawMap.has(target)) return target const proxy new Proxy(target, { get(target, key, receiver) { const res Reflect.get(target, key, receiver) track(target, key) // 依赖收集 return isObject(res) ? reactive(res) : res // 懒代理 }, set(target, key, value, receiver) { const oldValue target[key] const res Reflect.set(target, key, value, receiver) if (oldValue ! value) { trigger(target, key) // 触发更新 } return res }, deleteProperty(target, key) { const hadKey Object.prototype.hasOwnProperty.call(target, key) const res Reflect.deleteProperty(target, key) if (hadKey) trigger(target, key) return res } }) proxyMap.set(target, proxy) rawMap.set(proxy, target) return proxy }这里有两个关键点值得在断点里确认。第一是「懒代理」get里只有真正访问到嵌套对象时才递归reactive没访问就不代理这比 Vue2 初始化时递归遍历所有属性要省。第二是track和trigger的配对它们依赖一个全局的activeEffect来记录当前正在执行的副作用。你可以写一段最小测试在track和trigger里各打一个断点import { reactive, effect } from vue const state reactive({ foo: foo, bar: { a: 1 } }) effect(() { console.log(effect run:, state.foo) }) state.foo changed // 断点应命中 trigger state.bar.a 2 // 断点应命中 bar 的 track实测下来第一次执行effect时会命中track把state.foo和当前 effect 建立映射修改state.foo时命中trigger取出对应 effect 重新执行。而state.bar.a只有在effect里访问过bar之后才会被代理这就是懒代理的直观体现。toRefs的作用也在这里它把 reactive 对象的每个属性转成带 get/set 的 ref解构后依然保持响应性适合在setup里返回给模板。4. 编译优化模板如何变成带 PatchFlags 的渲染函数响应式解决的是「数据变了怎么知道」编译优化解决的是「知道了之后怎么少干活」。Vue3 的 compiler 位于packages/compiler-core和compiler-dom它把模板字符串解析成 AST再经过 transform 阶段做静态提升、补丁标记最后 generate 出渲染函数。你可以用vue/compiler-dom单独调用观察编译产物import { compile } from vue/compiler-dom const { code } compile( div pstatic text/p p{{ dynamic }}/p /div , { hoistStatic: true, prefixIdentifiers: true }) console.log(code)编译结果里你会看到几个关键产物。静态节点被提升到渲染函数外部只创建一次动态节点被加上PatchFlags比如TEXT表示只有文本会变PROPS表示只有属性会变。运行时patch阶段就靠这些标记跳过整棵树的 diff只更新标记过的部分。这就是「静态树提升」和「静态属性提升」的落地方式。在 VS Code 里验证这一步可以在packages/compiler-core/src/transform.ts的transform函数和packages/compiler-core/src/codegen.ts的generate函数下断点观察 AST 节点上的hoisted、patchFlag字段是怎么被赋值的。另一个值得跟的点是createElementVNode调用里的第四个参数它就是 PatchFlags 的传递位置。你可以对比开启和关闭hoistStatic两种编译结果差异非常直观关闭时静态节点也在渲染函数内部每次渲染都会重新创建。5. 运行时协作从 render 到 patch 的完整调用链把编译和响应式接起来的是 runtime-core。组件挂载时setup返回的响应式数据被effect包裹渲染函数执行时访问数据触发track数据变化触发trigger进而重新执行渲染函数产生新的 VNode 树最后进入patch做最小更新。这条链路可以在packages/runtime-core/src/renderer.ts里完整跟下来。关键断点建议打在这几个位置baseCreateRenderer返回的render函数、patch函数、processElement里的patchElement。当你修改一个动态文本时断点会走到patchElement此时对比新旧 VNode 的patchFlag如果只有TEXT就只更新el.textContent不会去 diff 子节点。这就是编译优化和运行时协作的结果编译器提前告诉运行时「哪里可能变」运行时据此走快速路径。用一个带动态文本和静态子节点的组件做验证template div pstatic/p p{{ msg }}/p /div /template script setup import { ref } from vue const msg ref(hello) setTimeout(() { msg.value world }, 2000) /script在patchElement断点处观察第一次挂载走mountElement2 秒后更新只走patchElement且patchFlag为TEXT静态的pstatic/p完全没进 diff。如果你需要对照某段运行时代码的语义可以用 TaoToken 的模型对话做即时解释入口是 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 把断点处的调用栈贴进去问比翻文档快。6. 本篇常见错排查断点不中、代理不触发、编译产物对不上读源码时最容易卡在几个固定位置我按出现频率列一下。第一类问题是断点不中通常是因为 sourcemap 没映射到源码或者构建产物是生产版本。解决方式是确认pnpm build vue -f global -d用了-d并检查launch.json的sourceMapPathOverrides路径。第二类问题是reactive改了数据但effect没重跑常见原因是修改的属性在 effect 里没被访问过或者用了toRefs解构后丢了响应性。可以在trigger里打印target和key确认是否命中了正确的依赖集合。第三类问题是编译产物和预期不一致比如静态节点没被提升。这通常是hoistStatic没开或者模板里有动态绑定导致整块无法静态化。用vue/compiler-dom的compile单独跑一遍对比code输出最直接。第四类问题是patchFlag为 0 却仍然走了完整 diff检查是否在模板里用了v-bindobj这类动态属性展开它会让编译器无法精确标记。第五类问题是调试时改了源码但没重新构建Vue3 的包是构建产物改packages下的 ts 后必须重新pnpm build才生效。如果你在接入或排障过程中需要查 API 细节可以走接入文档 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 需要管理密钥时用 API Keys 页面 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。长期做源码级调试和 Agent 辅助编码的话Coding Plan 入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 配合断点调试能省不少来回查文档的时间。