一、先明确你要研究的“Vue”
Vue 3 源码主要可以分成几条链路:
模板编译:
template
→ parse
→ transform
→ codegen
→ render 函数
组件运行时:
createApp
→ mount
→ render
→ patch
→ component mount/update
响应式系统:
reactive/ref
→ track
→ trigger
→ effect
→ scheduler
DOM 更新:
响应式变更
→ effect runner
→ componentUpdateFn
→ render
→ patch
→ hostInsert/hostPatchProp
服务端渲染:
组件/虚拟节点
→ renderToString
不要试图一次理解所有模块。建议先选一条主线。
二、最推荐的研究顺序
1. 先从响应式系统开始
响应式是 Vue 的基础,而且相对独立,适合建立整体理解。
重点文件大致是:
packages/reactivity/src/
├── effect.ts
├── reactive.ts
├── baseHandlers.ts
├── ref.ts
├── computed.ts
├── watch.ts
└── collectionHandlers.ts
先搞清楚最基本的模型:
const state = reactive({ count: 0 })
effect(() => {
console.log(state.count)
})
state.count++
内部可以抽象成:
effect 执行
→ 读取 state.count
→ Proxy get
→ track(target, key)
→ 建立依赖关系
state.count++
→ Proxy set
→ trigger(target, key)
→ 找到相关 effect
→ 调度或重新执行
建议自己画出依赖关系:
targetMap
└── target
└── key
└── Set<ReactiveEffect>
然后分别研究:
ReactiveEffectactiveEffecttracktriggereffect 嵌套
effect 停止
computed 的懒执行与缓存
watch 如何创建依赖与调度任务
scheduler 与
nextTick
不要一开始就看所有边界情况,先实现一个简化版:
const targetMap = new WeakMap()
let activeEffect
function effect(fn) {
const runner = () => {
activeEffect = runner
fn()
activeEffect = undefined
}
runner()
return runner
}
function track(target, key) {
// target -> key -> effects
}
function trigger(target, key) {
// 找到 effects 并重新执行
}
自己写过一遍后,再回头看 Vue 的源码,理解会明显加深。
2. 再看虚拟节点与渲染器
重点关注:
packages/runtime-core/src/
├── vnode.ts
├── renderer.ts
├── component.ts
├── componentProps.ts
├── componentEmits.ts
├── componentSlots.ts
├── scheduler.ts
└── apiLifecycle.ts
先研究最简单的流程:
const vnode = h('div', { class: 'a' }, 'hello')
render(vnode, container)
需要理解:
h/createVNode
→ vnode
render
→ patch(null, vnode, container)
patch
→ 根据 vnode.type 判断处理方式
processElement
→ mountElement
mountElement
→ createElement
→ setElementText / mountChildren
→ patchProp
→ insert
然后研究更新:
旧 vnode + 新 vnode
→ patch
→ patchElement
→ patchProps
→ patchChildren
重点不要只看“做了什么”,而要问:
为什么要有 vnode?
shapeFlag和patchFlag分别解决什么问题?type、key、el、component、children分别表示什么?为什么 patch 需要区分 element、component、text、fragment、teleport?
renderer 为什么通过 host 操作抽象,而不是直接操作 DOM?
3. 再研究组件挂载和更新
组件部分建议只追踪一个最简单的组件:
const App = {
setup() {
const count = ref(0)
return { count }
},
render() {
return h('div', this.count)
}
}
核心链路:
createApp(App)
→ mount
→ render(vnode, container)
→ processComponent
→ mountComponent
→ createComponentInstance
→ setupComponent
→ setupStatefulComponent
→ setup()
→ setupRenderEffect
→ component.render()
→ patch 子树
更新时:
state 发生变化
→ 组件 render effect 被触发
→ scheduler 将更新放入队列
→ flushJobs
→ componentUpdateFn
→ render 新 vnode
→ patch(oldSubTree, newSubTree)
组件源码里比较容易混乱的是各种上下文:
instancesetupStatepropsattrsslotsctxproxyrenderContext
建议重点理解一句话:
组件实例保存组件运行时状态,组件代理负责把模板/渲染函数中的访问转发到正确的数据来源。
例如:
this.count
可能依次尝试从:
setupState
props
data
ctx
中获取。
4. 最后看编译器
编译器非常适合在运行时基础较牢后再看,否则容易陷入 AST 细节。
整体流程:
template
→ baseParse
→ transform
→ transformElement
→ transformText
→ transformExpression
→ generate
→ render function code
用非常简单的模板开始:
<div>{{ msg }}</div>
然后研究它如何变成近似:
(_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg)))
编译器要重点理解几个概念:
AST 节点类型
transform的进入与退出阶段codegenNode静态节点分析
patch flag
block tree
hoist
openBlockcreateElementBlock
不要先背代码生成器。先理解编译器为什么要做优化:
普通 diff:
运行时检查大量节点
编译器优化:
提前标记动态节点
→ 运行时只关注动态部分
三、建议建立三张图
1. 响应式图
reactive object
→ Proxy
→ track
→ targetMap
→ effect
set
→ trigger
→ scheduler
→ effect
2. 组件更新图
setup()
→ render effect
→ render()
→ subTree
→ patch()
→ DOM
state mutation
→ trigger
→ scheduler
→ update component
3. 编译图
template
→ AST
→ transform
→ codegen AST
→ render function
→ vnode
→ renderer
如果这三张图能够脱离源码讲出来,说明已经形成了整体模型。
四、一定要使用调试,而不是只阅读
建议自己准备一个最小源码调试项目,给这些位置打断点或加日志:
reactive:
track
trigger
ReactiveEffect.run
renderer:
render
patch
mountElement
patchElement
patchChildren
patchKeyedChildren
component:
createComponentInstance
setupComponent
setupRenderEffect
componentUpdateFn
scheduler:
queueJob
flushJobs
每次只研究一个问题,例如:
问题:count++ 后 DOM 为什么更新?
追踪:
count++
→ ref.value setter
→ triggerRefValue
→ triggerEffects
→ scheduler
→ queueJob
→ flushJobs
→ componentUpdateFn
→ render
→ patch
问题:数组更新为什么复杂?
专门测试:
items.push(1)
items.splice(0, 1)
items.length = 0
items[0] = value
然后观察:
数组方法是否被包装
length如何处理哪些 effect 被触发
迭代依赖如何记录
Map、Set为什么需要特殊 handler
五、建议按“问题”阅读,而不是按“文件”阅读
不要问:
这个文件有多少代码?
而要问:
一个响应式属性是如何找到依赖它的 effect 的?
一个组件第一次挂载经过哪些函数?
一个组件状态变化后为什么不会立即同步更新 DOM?
patch 如何判断两个 vnode 是否是同一个节点?
keyed children 的最长递增子序列解决了什么问题?
nextTick为什么能拿到更新后的 DOM?computed为什么不会每次读取都重新计算?watch监听一个对象时,依赖是如何被遍历出来的?模板中的静态节点为什么可以提升?
编译器如何告诉运行时哪些节点是动态的?
每次只解决一个问题,并记录调用栈和数据结构。
六、源码阅读时重点关注的数据结构
Vue 源码难的地方,往往不是函数本身,而是中间数据结构。
建议重点跟踪:
响应式
WeakMap → Map → Set
VNode
{
__v_isVNode,
type,
props,
key,
ref,
children,
shapeFlag,
patchFlag,
dynamicChildren,
component,
el
}
ComponentInstance
{
vnode,
type,
parent,
appContext,
root,
next,
subTree,
update,
render,
setupState,
props,
attrs,
slots,
ctx,
proxy,
emit
}
Scheduler
queue
→ pendingPreFlushCbs
→ pendingPostFlushCbs
→ currentFlushPromise
理解这些结构,比记住几十个函数名更重要。
七、推荐的实验顺序
可以按下面的顺序写小实验:
reactive + effectref + effectcomputedwatchh + render普通元素挂载
文本节点更新
属性更新
子节点更新
keyed children diff
函数组件
有状态组件
setup返回 render 函数setup返回状态对象组件更新调度
nextTickprovide/injectTeleportSuspensetemplate 编译
每个实验最好都包含:
源码
→ 调用栈
→ 关键对象变化
→ 最终 DOM
八、常见误区
1. 线性通读所有源码
这通常只能获得“看过”的感觉,无法形成调用模型。
2. 一开始研究边界功能
比如:
SSR
Suspense
Teleport
KeepAlive
自定义 renderer
编译器全部优化
这些应该放到主链路之后。
3. 只看函数,不看调用者和调用时机
同一个函数在不同阶段调用,含义可能不同。尤其是:
patcheffectqueueJobsetupRenderEffectnormalizeVNode
4. 忽略平台抽象
Vue runtime-core 并不直接依赖浏览器 DOM。要同时区分:
runtime-core:通用渲染逻辑
runtime-dom:DOM 平台实现
runtime-test:测试平台实现
5. 只记结论,不做验证
例如“Vue 有批量更新”不够,要验证:
state.a++
state.b++
console.log('after mutations')
await nextTick()
观察 effect 执行次数和 DOM 更新时机。
九、一个适合重新开始的最小主线
如果你想马上重新读,建议只追踪这个例子:
const App = {
setup() {
const count = ref(0)
return {
count,
inc() {
count.value++
}
}
},
render() {
return h('button', {
onClick: this.inc
}, String(this.count))
}
}
按以下顺序追踪:
createApp
→ mount
→ render
→ processComponent
→ mountComponent
→ setupComponent
→ setup()
→ setupRenderEffect
→ render()
→ h/createVNode
→ patch
→ mountElement
→ ref 依赖收集
→ click
→ ref.value++
→ trigger
→ scheduler
→ component update
→ patch
→ 更新文本
这条链路同时覆盖了:
应用创建
组件实例
setup
ref
render
vnode
renderer
effect
scheduler
DOM 更新
把这条链路真正搞懂后,再扩展到 compiler、diff、watch、生命周期等模块,会比重新通读源码有效很多。