一、先明确你要研究的“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>

然后分别研究:

  • ReactiveEffect

  • activeEffect

  • track

  • trigger

  • effect 嵌套

  • 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

重点不要只看“做了什么”,而要问:

  1. 为什么要有 vnode?

  2. shapeFlag 和 patchFlag 分别解决什么问题?

  3. type、key、el、component、children 分别表示什么?

  4. 为什么 patch 需要区分 element、component、text、fragment、teleport?

  5. 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)

组件源码里比较容易混乱的是各种上下文:

  • instance

  • setupState

  • props

  • attrs

  • slots

  • ctx

  • proxy

  • renderContext

建议重点理解一句话:

组件实例保存组件运行时状态,组件代理负责把模板/渲染函数中的访问转发到正确的数据来源。

例如:

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

  • openBlock

  • createElementBlock

不要先背代码生成器。先理解编译器为什么要做优化:

普通 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

理解这些结构,比记住几十个函数名更重要。


七、推荐的实验顺序

可以按下面的顺序写小实验:

  1. reactive + effect

  2. ref + effect

  3. computed

  4. watch

  5. h + render

  6. 普通元素挂载

  7. 文本节点更新

  8. 属性更新

  9. 子节点更新

  10. keyed children diff

  11. 函数组件

  12. 有状态组件

  13. setup 返回 render 函数

  14. setup 返回状态对象

  15. 组件更新调度

  16. nextTick

  17. provide/inject

  18. Teleport

  19. Suspense

  20. template 编译

每个实验最好都包含:

源码
→ 调用栈
→ 关键对象变化
→ 最终 DOM

八、常见误区

1. 线性通读所有源码

这通常只能获得“看过”的感觉,无法形成调用模型。

2. 一开始研究边界功能

比如:

  • SSR

  • Suspense

  • Teleport

  • KeepAlive

  • 自定义 renderer

  • 编译器全部优化

这些应该放到主链路之后。

3. 只看函数,不看调用者和调用时机

同一个函数在不同阶段调用,含义可能不同。尤其是:

  • patch

  • effect

  • queueJob

  • setupRenderEffect

  • normalizeVNode

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、生命周期等模块,会比重新通读源码有效很多。