1. 为什么选择学习 Vue 3 源码
作为一名长期使用 Vue 的前端开发者,我最初接触 Vue 3 源码纯粹是为了应付面试。但当我真正深入其中后,发现这远比想象中有价值。通过源码学习,不仅能解决那些困扰已久的"黑盒问题",更能从根本上提升编程思维和架构能力。
记得第一次在项目中遇到响应式数据更新但视图不渲染的问题时,我花了整整两天时间在各种论坛寻找解决方案。如果当时理解响应式系统的底层原理,可能十分钟就能定位到是 reactive 对象被意外解构导致 Proxy 失效。这就是源码学习的实际意义 - 它让你从"碰运气调试"转变为"精准定位问题"。
2. 环境准备与源码获取
2.1 开发环境配置
在开始前,确保你的开发环境满足以下要求:
- Node.js 16 或更高版本(推荐使用 nvm 管理多版本)
- pnpm 7.x(Vue 团队推荐使用的包管理器)
- 现代浏览器(Chrome 或 Edge 最新版)
- IDE 推荐 VS Code 并安装 Volar 插件
注意:虽然 Vue 3 理论上支持 Node.js 14,但某些依赖可能要求更高版本。我曾因 Node.js 版本问题浪费过半天时间调试安装错误,强烈建议直接使用 Node.js 16+。
2.2 获取源码
通过以下命令克隆和初始化项目:
bash复制git clone https://github.com/vuejs/core.git
cd core
git checkout main
pnpm install
pnpm build
这里有几个关键点需要注意:
- 一定要切换到 main 分支,这是最新的稳定版代码
- 使用 pnpm 而不是 npm/yarn,因为项目配置了 workspaces
- build 命令会编译所有包,首次运行可能需要 3-5 分钟
2.3 调试环境搭建
为了更方便地调试源码,建议在 packages/vue 目录下创建测试文件:
javascript复制// packages/vue/test.js
import { createApp, ref } from '../vue/dist/vue.js'
const app = createApp({
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
},
template: `
<button @click="increment">{{ count }}</button>
`
})
app.mount('#app')
然后在 HTML 中引入这个文件,就可以在浏览器开发者工具中调试了。我习惯在 reactive.ts 和 effect.ts 的关键函数处设置断点,观察依赖收集和触发更新的过程。
3. 源码目录深度解析
3.1 核心模块架构
Vue 3 采用 monorepo 结构,主要模块分布在 packages 目录下:
code复制packages/
├── reactivity/ # 响应式系统核心
├── runtime-core/ # 平台无关的运行时
├── runtime-dom/ # 浏览器特定的运行时
├── compiler-core/ # 编译器核心
├── compiler-dom/ # 浏览器特定的编译器
├── vue/ # 主要入口
├── shared/ # 共享工具函数
这种模块化设计使得 Vue 3 可以更灵活地适应不同平台。比如你可以只使用 reactivity 模块,而不需要引入完整的 Vue。
3.2 学习优先级建议
根据我的经验,建议按以下顺序学习:
- reactivity(1-2周):理解响应式原理是基础
- runtime-core(2-3周):掌握组件渲染流程
- compiler-core(可选):了解模板编译过程
- 其他模块:按需学习
我曾尝试从 compiler 开始学习,结果发现难度陡增。后来调整顺序先攻 reactivity,学习曲线就平缓多了。
4. 响应式系统深入剖析
4.1 reactive 实现原理
reactive.ts 是响应式系统的核心,其关键代码如下:
typescript复制function reactive(target: object) {
// 如果已经是代理对象,直接返回
if (target[ReactiveFlags.RAW]) {
return target
}
// 创建代理
return createReactiveObject(
target,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: Target,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 使用 Proxy 创建响应式代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
这里有几个关键点:
- 使用 Proxy 进行代理,相比 Vue 2 的 defineProperty 能检测更多操作
- 对集合类型(Map/Set)使用不同的 handlers
- 通过 RAW 标记避免重复代理
常见误区:很多人以为 reactive 会递归代理所有嵌套对象,实际上它是懒代理的 - 只有访问到的属性才会被代理。这能显著提升性能。
4.2 依赖收集与触发更新
effect.ts 实现了 Vue 的依赖收集系统:
typescript复制let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
run() {
// 保存当前 activeEffect
try {
this.parent = activeEffect
activeEffect = this
return this.fn()
} finally {
activeEffect = this.parent
}
}
}
function track(target: object, key: unknown) {
if (activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
}
function trigger(target: object, key: unknown) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect.run())
}
这个系统的精妙之处在于:
- 通过全局 activeEffect 跟踪当前运行的 effect
- track 时建立 target → key → effect 的映射关系
- trigger 时找到对应 effect 重新执行
我曾遇到一个性能问题:在大型列表中频繁更新 reactive 对象导致卡顿。通过分析源码发现,没有合理使用 effect 作用域是主因。后来改用 effectScope 管理,性能提升了 3 倍。
4.3 ref 与 reactive 的对比
ref.ts 提供了对基本类型的响应式包装:
typescript复制function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = value
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this)
}
}
}
ref 与 reactive 的主要区别:
- ref 可以包装基本类型,reactive 只能处理对象
- ref 通过 .value 访问,reactive 直接访问属性
- ref 内部也使用 reactive 处理对象值
在实际项目中,我倾向于:基本类型用 ref,对象用 reactive。但对于需要解构的场景,toRefs 配合 reactive 更合适。
5. 运行时系统解析
5.1 组件渲染流程
runtime-core 中的 renderer.ts 定义了核心渲染逻辑:
typescript复制function baseCreateRenderer(
options: RendererOptions
): Renderer {
function render(vnode: VNode, container: RendererElement) {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
container._vnode = vnode
}
return {
render,
createApp: createAppAPI(render)
}
}
整个渲染流程可以概括为:
- createApp 创建应用实例
- mount 时编译模板为 render 函数(如果使用模板)
- render 函数执行生成 VNode
- patch 对比新旧 VNode 并更新 DOM
我曾通过重写简单版 patch 函数来理解 diff 算法,这个练习让我真正明白了 key 的重要性。
5.2 虚拟 DOM 与 diff 算法
Vue 3 的 diff 算法在 patchChildren 中实现:
typescript复制function patchChildren(
n1: VNode | null,
n2: VNode,
container: RendererElement
) {
// 获取新旧子节点
const c1 = n1 && n1.children
const c2 = n2.children
// 处理不同子节点类型
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本子节点
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组子节点 - 执行 diff
patchKeyedChildren(c1, c2, container)
}
}
function patchKeyedChildren(
c1: VNode[],
c2: VNode[],
container: RendererElement
) {
// 双端比较算法
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 1. 从头部开始比对
while (i <= e1 && i <= e2 && isSameVNodeType(c1[i], c2[i])) {
patch(c1[i], c2[i], container)
i++
}
// 2. 从尾部开始比对
while (i <= e1 && i <= e2 && isSameVNodeType(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container)
e1--
e2--
}
// 3. 处理新增/删除
if (i > e1) {
// 新增节点
} else if (i > e2) {
// 删除节点
} else {
// 4. 未知序列 - 使用 key 映射
const keyToNewIndexMap = new Map()
for (let j = i; j <= e2; j++) {
keyToNewIndexMap.set(c2[j].key, j)
}
// 移动和 patch 现有节点
for (let j = i; j <= e1; j++) {
const prevChild = c1[j]
const newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 移除旧节点
} else {
// patch 匹配的节点
}
}
}
}
这个算法有几个优化点:
- 双端比较快速处理头尾相同的情况
- 使用 key 建立映射关系,最大化复用节点
- 最长递增子序列优化移动操作
在实现树形组件时,我曾因不当使用 index 作为 key 导致渲染错误。通过研究这段代码,我彻底理解了 key 的正确用法。
5.3 异步更新与 nextTick
scheduler.ts 实现了 Vue 的异步更新队列:
typescript复制const queue: SchedulerJob[] = []
function queueJob(job: SchedulerJob) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs)
}
}
function flushJobs() {
isFlushPending = false
isFlushing = true
// 排序队列以保证:
// 1. 组件从父到子更新
// 2. 父组件更新期间卸载的子组件跳过
queue.sort((a, b) => getId(a) - getId(b))
try {
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
job()
}
} finally {
isFlushing = false
queue.length = 0
}
}
nextTick 的实现基于 Promise:
typescript复制const resolvedPromise = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
function nextTick(fn?: () => void): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(fn) : p
}
这个机制解释了为什么连续修改响应式数据不会导致多次渲染 - 所有变更会被批量处理。在我的一个项目中,利用这个特性优化了高频数据更新的性能。
6. 编译器工作原理
6.1 模板编译流程
compiler-core 将模板编译为 render 函数的过程:
- parse:将模板字符串解析为 AST
- transform:对 AST 进行转换和优化
- codegen:根据 AST 生成 render 函数代码
typescript复制function baseCompile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
// 1. 解析模板为 AST
const ast = parse(template, options)
// 2. 转换 AST
transform(ast, {
...options,
nodeTransforms: [
...(options.nodeTransforms || []),
transformIf,
transformFor
]
})
// 3. 生成代码
return generate(ast, options)
}
6.2 静态提升优化
Vue 3 的编译器会识别静态内容并进行提升:
typescript复制const hoistStatic = (node: RootNode | TemplateChildNode) => {
if (node.type === NodeTypes.ELEMENT) {
if (isStaticExp(node)) {
node.codegenNode = context.hoist(node.codegenNode!)
}
}
}
这使得静态内容只在初始化时创建一次,而不是每次渲染都重新创建。在一个大型表格组件中,应用这个优化后渲染性能提升了约 20%。
7. 手写实现核心功能
7.1 简易响应式系统
javascript复制const targetMap = new WeakMap()
let activeEffect = null
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key)
return result
}
})
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
这个简化版实现了响应式核心功能,帮助我理解了依赖收集的基本原理。
7.2 简易虚拟 DOM 渲染
javascript复制function h(type, props, children) {
return { type, props, children }
}
function mount(vnode, container) {
const el = document.createElement(vnode.type)
// 处理 props
if (vnode.props) {
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key])
}
}
// 处理 children
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, el)
})
}
}
container.appendChild(el)
vnode.$el = el
}
function patch(n1, n2, container) {
// 简单实现 - 实际 Vue 使用更复杂的 diff 算法
if (n1.type !== n2.type) {
container.removeChild(n1.$el)
mount(n2, container)
} else {
const el = n2.$el = n1.$el
// 更新 props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
el.setAttribute(key, newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 更新 children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 简化的数组 diff - 实际 Vue 使用 key 优化
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i], el)
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child.$el)
})
}
}
}
}
}
这个实现虽然简单,但包含了虚拟 DOM 的核心概念。通过手写这些代码,我对 Vue 的内部工作原理有了更深的理解。
8. 学习路线与面试准备
8.1 30 天学习计划
基于我的经验,建议这样安排学习:
第一周:响应式系统
- 第1天:reactive 实现原理
- 第2天:ref 实现原理
- 第3天:effect 和依赖收集
- 第4天:computed 和 watch
- 第5天:响应式工具函数
- 第6天:手写简易响应式系统
- 第7天:复习与面试题练习
第二周:运行时核心
- 第8天:createApp 和 mount 流程
- 第9天:render 和 patch 过程
- 第10天:组件实例生命周期
- 第11天:虚拟 DOM 结构
- 第12天:diff 算法原理
- 第13天:异步更新队列
- 第14天:手写简易渲染器
第三周:编译器和综合
- 第15-17天:模板编译流程
- 第18-19天:静态提升优化
- 第20-21天:源码调试技巧
- 第22-23天:性能优化策略
- 第24-25天:综合面试题准备
- 第26-27天:模拟面试练习
- 第28-30天:重点复习与补漏
8.2 常见面试题解析
问题:Vue 3 的响应式系统是如何工作的?
回答要点:
- reactive 使用 Proxy 代理对象
- get 操作时通过 track 收集依赖
- set 操作时通过 trigger 触发更新
- effect 作为副作用函数,执行时会被 activeEffect 记录
- 依赖关系存储在全局 targetMap 中
问题:ref 和 reactive 有什么区别?
回答要点:
- ref 可以包装基本类型,reactive 只能处理对象
- ref 通过 .value 访问,reactive 直接访问属性
- ref 内部对对象值也使用 reactive
- 模板中 ref 会自动解包,不需要 .value
- 解构 reactive 对象会失去响应性,而 toRefs 可以保持
问题:Vue 3 的 diff 算法有什么优化?
回答要点:
- 同层比较,不跨层级
- 双端比较快速处理头尾相同情况
- 基于 key 的节点复用
- 最长递增子序列优化移动操作
- 静态标记提升 diff 效率
9. 调试技巧与实用工具
9.1 源码调试方法
-
浏览器调试:
- 在源码关键位置添加 debugger 语句
- 使用 sourcemap 映射编译后代码
- 通过调用栈分析执行流程
-
VS Code 调试:
json复制{ "type": "node", "request": "launch", "name": "Debug Vue", "program": "${workspaceFolder}/packages/vue/test.js", "skipFiles": ["<node_internals>/**"] } -
自定义日志:
typescript复制// 在 reactive.ts 中添加调试日志 function track(target: object, key: unknown) { console.log(`Tracking ${String(key)} on`, target) // ... }
9.2 实用工具函数
Vue 3 的 shared 模块提供了许多实用工具:
typescript复制// 判断类型
function isObject(value: unknown): boolean
function isFunction(value: unknown): boolean
// 工具函数
function hasChanged(value: any, oldValue: any): boolean
function def(obj: object, key: string | symbol, value: any)
这些工具函数经过精心优化,可以直接借鉴到自己的项目中。
10. 学习资源推荐
-
官方资源:
- Vue 3 官方文档
- Vue RFCs(了解设计决策过程)
-
源码分析:
-
视频教程:
- B 站搜索"Vue 3 源码解析"
- 慕课网相关实战课程
-
社区讨论:
- Vue 官方 Discord 频道
- GitHub 项目 issues 区
-
配套工具:
- Vue DevTools 最新版
- Volar VS Code 插件
学习源码的过程就像探索一座精心设计的建筑。最初可能只看到外观,但随着深入,你会逐渐理解每个设计决策背后的考量。这种理解不仅能帮助你在面试中脱颖而出,更能提升日常开发中的问题解决能力。