Vue 原理与源码学习笔记
适合目标:系统理解 Vue 3 的底层运行机制,能把响应式系统、编译、渲染、Diff、调度这些高频源码面试题讲清楚。
学习定位:这一份偏“能解释原理、能看懂主干源码、能回答底层问题”。
学习原则:先搭地图,再看实现;先理解数据流和渲染流,再读源码细节;先掌握主干概念,再补优化机制。
目录
- 学习总览
- 看源码前先建立地图
- Vue 整体运行流程
- 响应式系统
- ref、computed、watch 原理
- 编译器与模板编译
- 渲染器与虚拟 DOM
- Patch 与 Diff
- 调度器与异步更新
- 组件更新流程
- 内置能力原理
- Vue 3 设计升级点
- 常见源码面试题
- 8 小时源码学习规划
- 一页速记总结
- 背诵口诀
- 源码阅读路线
1. 学习总览
1.1 为什么要学 Vue 原理
只会使用 Vue 和能讲清 Vue 的底层,是两个层次。
学原理的价值:
- 面试能解释“为什么”
- 写组件时更容易避坑
- 性能优化更有依据
- 看到奇怪更新行为时更容易定位问题
1.2 Vue 原理到底学什么
Vue 底层最核心的主线可以记成:
响应式 -> 依赖收集 -> 触发更新 -> 生成虚拟节点 -> patch DOM
你真正需要掌握的是:
- Vue 如何让对象变成响应式
- 依赖是怎么被收集的
- 数据变化后为什么视图会更新
- 模板为什么能变成渲染函数
- 新旧节点如何比较
- 为什么 DOM 更新是异步批量的
1.3 Vue 源码学习主线
可以记成:
template -> compile -> render function -> vnode -> patch -> DOM
同时还有另一条状态主线:
reactive data -> track -> trigger -> scheduler -> component update
这两条线合起来,才是 Vue 的完整更新机制。
1.4 面试里最常考的底层话题
- Vue 3 响应式为什么用 Proxy
ref和reactive底层区别computed为什么有缓存watch和watchEffect区别- Vue 为什么要异步更新 DOM
nextTick原理是什么- Vue 3 Diff 做了什么优化
- Vue 模板是怎么编译的
2. 看源码前先建立地图
2.1 Vue 不是一个单独文件
Vue 3 源码是分模块组织的。
理解层面可以先记住几个重要部分:
reactivityruntime-coreruntime-domcompiler-corecompiler-dom
2.2 这些模块负责什么
reactivity
负责响应式能力,例如:
reactiverefcomputedeffectwatch
runtime-core
负责运行时核心逻辑,例如:
- 组件初始化
- 虚拟 DOM
- patch 流程
- 调度
runtime-dom
负责和浏览器 DOM 交互。
compiler-core
负责把模板编译成渲染函数的核心逻辑。
compiler-dom
处理浏览器 DOM 相关模板编译细节。
2.3 阅读源码最容易犯的错
- 上来就啃 Proxy 细节
- 看一堆函数却不知道在哪个阶段
- 把 Vue 2 和 Vue 3 的实现混在一起
- 只背结论,不知道流程顺序
2.4 正确阅读策略
推荐顺序:
- 先理解整体流程
- 再看响应式
- 再看编译
- 再看渲染和 patch
- 最后看调度与优化
3. Vue 整体运行流程
3.1 从模板到 DOM 的大流程
Vue 组件运行可以先简化成:
- 读取组件定义
- 模板编译成 render 函数
- render 函数执行生成 vnode
- patch 把 vnode 挂载到真实 DOM
- 响应式数据变化后重新执行 render
- 对新旧 vnode 做比较并更新 DOM
3.2 什么是 vnode
vnode 就是虚拟节点对象,用来描述应该渲染成什么。
它通常包含:
- 类型
- props
- children
- key
- patch 相关标记
3.3 Vue 的更新主线
可以浓缩成一句话:
数据变化 -> 触发副作用函数重新执行 -> 生成新 vnode -> patch
3.4 为什么要把编译和运行时分开
因为模板编译发生在更早阶段,而渲染更新发生在运行阶段。
好处:
- 可以提前做优化
- 运行时负担更小
- 模板表达能力更强
4. 响应式系统
Vue 3 最核心的底层能力就是响应式系统。
4.1 响应式系统解决什么问题
它解决的是:
谁依赖了某个数据,数据变了要通知谁重新执行
这里有两个核心动作:
track:依赖收集trigger:依赖触发
4.2 effect 是什么
可以把 effect 理解成:
一个会在依赖变化时重新执行的副作用函数
在组件渲染层面,组件的 render 过程本质上也会被包装成响应式 effect。
4.3 reactive 的本质
reactive 会返回一个 Proxy 代理对象。
当你:
- 读取属性时,触发
get,进行依赖收集 - 修改属性时,触发
set,进行依赖通知
4.4 为什么 Vue 3 用 Proxy
这是超级高频题。
答题思路:
Proxy能代理整个对象,而不是逐个属性定义 getter/setter- 对新增属性、删除属性、数组索引、
length等支持更自然 - API 更统一,扩展性更好
4.5 依赖收集怎么理解
当某个响应式属性在 effect 中被读取时:
- 当前激活的 effect 会被记录下来
- 这个属性和这个 effect 建立依赖关系
可以理解成:
属性记住了哪些 effect 用过自己
4.6 触发更新怎么理解
当响应式属性被修改时:
- 找到依赖它的 effect 集合
- 依次通知这些 effect 重新执行
4.7 targetMap 怎么理解
源码层可以理解成一个多层映射结构:
- 某个目标对象
- 对应某个 key
- key 下存依赖这个属性的 effect 集合
可以粗略理解为:
target -> key -> dep(Set<effect>)
4.8 为什么要用 Set 存依赖
因为:
- 去重方便
- 查找和添加方便
- 不会重复收集同一个 effect
4.9 响应式系统记忆口诀
读的时候收集依赖,写的时候触发依赖。
5. ref、computed、watch 原理
5.1 ref 原理怎么理解
ref 本质上是一个带 .value 的响应式包装对象。
为什么要这样设计:
- 基本类型不能直接被 Proxy 代理
- 用一个对象壳把值包起来,就能统一响应式处理
所以你可以把它理解成:
{
value: 某个值
}
只不过这个 value 的读取和设置被响应式系统接管了。
5.2 reactive 和 ref 底层区别
reactive主要代理对象本身ref主要通过.value这个访问点做依赖收集和触发
5.3 computed 为什么有缓存
computed 的核心是:
- 内部也基于 effect
- 但它是“惰性求值”的
- 只有依赖变化时才会标记为脏
- 下次读取时才重新计算
你可以把它理解成:
- 第一次读,执行 getter,缓存结果
- 依赖变了,不马上重算,只把它标记为脏
- 下次再读时,才真正重新计算
这就是缓存的来源。
5.4 watch 原理怎么理解
watch 会:
- 监听指定源
- 比较新旧值
- 当值变化时执行回调
它比 watchEffect 更明确,因为监听目标是显式指定的。
5.5 watchEffect 原理怎么理解
watchEffect 在执行过程中自动收集依赖。
可以理解成:
- 先执行一次函数
- 函数里读到的响应式数据都会被收集
- 这些依赖变化时,函数重新执行
5.6 watch 和 watchEffect 区别
watch关注“监听谁”watchEffect关注“执行过程中用了谁”watch更可控watchEffect更自动
5.7 cleanup 怎么理解
无论是 watch 还是 watchEffect,都常和清理逻辑相关。
典型场景:
- 取消上一次请求
- 清除定时器
- 解绑订阅
5.8 记忆口诀
ref 是 value 壳,computed 是带缓存的惰性 effect,watch 是显式监听,watchEffect 是自动收集。
6. 编译器与模板编译
6.1 为什么 Vue 需要编译器
因为 Vue 支持模板:
<div>{{ msg }}</div>
模板本身不能直接运行,必须先变成 JavaScript 渲染函数。
6.2 编译大流程
模板编译可以理解成三步:
parse:把模板解析成 ASTtransform:对 AST 做转换和优化generate:生成 render 函数字符串 / 代码
6.3 AST 是什么
AST 是抽象语法树。
你可以把它理解成:
模板结构的树形数据表示
6.4 transform 阶段做什么
- 分析节点类型
- 处理指令
- 标记动态节点
- 做一些编译优化准备
6.5 generate 阶段做什么
把处理后的 AST 生成 render 函数。
6.6 render 函数是什么
render 函数本质上是:
根据当前状态,返回 vnode 树的函数
6.7 编译优化为什么重要
Vue 3 的重要优势之一,就是编译阶段能提前识别:
- 哪些节点是静态的
- 哪些节点是动态的
- 哪些片段值得单独追踪
这样运行时 patch 的成本会更低。
6.8 Patch Flags 怎么理解
Vue 3 会在编译阶段给某些动态节点打上补丁标记。
作用:
- 让运行时更快知道哪里可能变
- 减少无意义对比
6.9 Block Tree 怎么理解
Vue 3 会把真正动态的节点集中关注,帮助运行时优化更新范围。
你不必死记细节,但面试里可以答:
- Vue 3 通过编译阶段分析动态节点
- 配合 patch flags 和 block tree,减少运行时无效 Diff
6.10 编译主线口诀
模板先 parse 成 AST,再 transform 做分析优化,最后 generate 出 render。
7. 渲染器与虚拟 DOM
7.1 渲染器在做什么
渲染器负责把 vnode 变成真实界面。
主要工作:
- 首次挂载
- 更新 patch
- 卸载节点
7.2 runtime-core 和 runtime-dom 的关系
runtime-core
更偏平台无关的核心渲染逻辑。
runtime-dom
负责浏览器 DOM 环境的实际操作。
7.3 为什么要有虚拟 DOM
虚拟 DOM 不是为了“绝对更快”,而是为了:
- 提供跨平台抽象
- 让声明式渲染更自然
- 让更新流程可以统一处理
7.4 vnode 和真实 DOM 的关系
- vnode 是描述对象
- 真实 DOM 是浏览器节点
- patch 负责把 vnode 的变化映射到真实 DOM
7.5 挂载过程怎么理解
首次渲染时:
- 执行 render,拿到 vnode
- 遍历 vnode
- 创建真实节点
- 插入页面
7.6 更新过程怎么理解
更新时:
- 再次执行 render,拿到新 vnode
- 和旧 vnode 比较
- 找出差异
- 最小化更新真实 DOM
7.7 渲染器记忆口诀
render 产出 vnode,patch 负责挂载和更新。
8. Patch 与 Diff
Vue 的 Diff 面试高频程度很高,尤其是 key、最长递增子序列、静态提升这些点。
8.1 Patch 是什么
patch 可以理解成:
根据新旧 vnode 的差异,把真实界面更新到最新状态
8.2 Diff 为什么需要 key
在列表更新中,如果没有稳定 key,框架很难正确识别每个节点身份。
有 key 的好处:
- 节点复用更准确
- 状态更稳定
- 移动成本更容易判断
8.3 Vue 3 Diff 的常见思路
理解版可以记成:
- 先比头
- 再比尾
- 中间乱序部分再进一步处理
- 必要时借助最长递增子序列减少移动
8.4 最长递增子序列 LIS 为什么会出现
在处理中间乱序列表时,Vue 3 会尽量找出“已经相对稳定、不需要移动”的那部分节点。
这个“尽量少移动”的优化,背后就会用到最长递增子序列思想。
你面试里不用展开算法证明,但要会说:
- Vue 3 在 keyed children Diff 中会尽量减少节点移动
- LIS 是为了找出不需要移动的最长稳定子序列
8.5 Vue 3 Diff 优化点怎么答
常见答法:
- 头尾指针优化
- 针对稳定 key 做更高效比较
- 对中间乱序部分结合映射表和 LIS 优化移动
- 配合编译阶段 patch flags 减少无意义对比
8.6 patchChildren 怎么理解
不同 children 形态会有不同处理:
- 文本
- 数组
- 空
更新时要考虑新旧 children 之间的转换。
8.7 为什么 Vue 3 比 Vue 2 更容易优化
因为 Vue 3 不仅靠运行时 Diff,还结合了编译时信息。
8.8 Diff 记忆口诀
稳定 key 定身份,头尾先比较,中间再精算,LIS 减少移动。
9. 调度器与异步更新
9.1 Vue 为什么不每次改数据都立刻改 DOM
如果每次小改动都立刻刷新 DOM,会带来:
- 频繁重复更新
- 性能浪费
- 同一轮里很多无意义渲染
所以 Vue 选择:
把同一轮更新收集起来,再批量执行
9.2 调度器解决什么问题
调度器负责:
- 收集组件更新任务
- 去重
- 排序
- 在合适时机统一执行
9.3 job 队列怎么理解
当组件需要更新时,不一定立刻重新渲染,而是:
- 生成一个更新 job
- 放入队列
- 在微任务时机统一刷新
9.4 为什么 nextTick 通常和微任务有关
因为 Vue 的异步刷新通常会尽量借助微任务队列,让更新更快但又能合并。
所以 nextTick 可以理解成:
等当前这轮更新队列刷新完后,再执行回调
9.5 批量更新的收益
- 减少重复 render
- 减少重复 patch
- 同一轮逻辑内更容易拿到稳定的最终结果
9.6 nextTick 原理答题模板
- Vue 会把更新任务异步批量处理
nextTick本质是等待这一轮 DOM 更新完成- 常借助微任务来安排回调执行
9.7 调度器口诀
更新先进队列,统一去重刷新,nextTick 等这一轮刷完。
10. 组件更新流程
10.1 组件为什么会更新
常见触发源:
- 自身响应式状态变化
- 父组件传入的 props 变化
- Context 类共享依赖变化,如 provide/inject 链上的值变化
- 强制刷新或其他内部机制触发
10.2 组件更新的大流程
可以理解成:
- 状态变化
trigger找到组件渲染 effect- 交给调度器排队
- effect 重新执行
- 再次调用 render 生成新 vnode
- patch 新旧 vnode
- 更新真实 DOM
10.3 组件渲染 effect
组件的渲染过程,本质上也会包装成一个 effect。
这样当模板里依赖的响应式数据变了,组件就知道自己该重新渲染。
10.4 父子更新关系
更新时通常要注意:
- 父组件变了,子组件可能也参与更新
- 但如果 props 没变、依赖没变,也可能跳过某些无意义工作
10.5 为什么局部更新重要
Vue 的性能优化很多时候不是“更快的算法”,而是:
让真正需要更新的范围尽量小
11. 内置能力原理
11.1 KeepAlive 原理化理解
KeepAlive 的核心不是普通显示隐藏,而是:
- 缓存组件实例
- 缓存对应子树
- 切换时避免重新挂载
11.2 Teleport 原理化理解
Teleport 让 vnode 的逻辑归属还在当前组件树里,但真实 DOM 可以挂到别处。
11.3 Suspense 原理化理解
当组件存在异步依赖还没准备好时,可以先显示 fallback,等就绪后再切换正式内容。
11.4 Transition 原理化理解
在 vnode 进入和离开时,协调 class 和时机,帮助动画过渡。
11.5 为什么这些内置能力重要
因为它们体现了 Vue 对真实 UI 场景的统一抽象,而不只是单纯模板渲染。
12. Vue 3 设计升级点
12.1 从 Vue 2 到 Vue 3 的核心升级
从原理视角看,重点是:
- 响应式系统升级为 Proxy
- Composition API 带来更强逻辑组织能力
- 编译优化更强
- 渲染和 Diff 优化更细
- TypeScript 体验更好
12.2 为什么 Vue 3 编译优化更重要
因为 Vue 3 不只是运行时框架,它更强调:
编译时尽可能多做分析,运行时尽可能少做无效工作
12.3 为什么说 Vue 3 更适合大型项目
可以这样答:
- Composition API 更适合复杂逻辑拆分
- TS 支持更友好
- 编译和运行时整体优化更成熟
13. 常见源码面试题
13.1 Vue 3 为什么用 Proxy
答:
Proxy可以拦截更完整的对象操作- 对新增、删除、数组索引等场景支持更自然
- 相比
Object.defineProperty更灵活
13.2 computed 为什么有缓存
答:
computed内部基于 effect- 采用惰性求值
- 依赖不变时直接复用旧结果
- 依赖变化时只标记脏,等下次读取再重算
13.3 watch 和 watchEffect 区别
答:
watch显式指定监听源watchEffect自动收集依赖watch更可控watchEffect更适合快速副作用联动
13.4 nextTick 为什么存在
答:
- Vue 会合并同一轮更新并异步刷新 DOM
nextTick让你在 DOM 更新完成后执行逻辑
13.5 Vue 模板是怎么变成 render 的
答:
- 模板先被解析成 AST
- 再做 transform 分析和优化
- 最后 generate 成 render 函数
13.6 Vue 3 Diff 有什么优化
答:
- 头尾指针优化
- 中间乱序比较优化
- 使用 LIS 尽量减少移动
- 配合编译时 patch flags 减少无意义比较
13.7 为什么 Vue 要异步更新
答:
- 避免同一轮里重复渲染
- 合并多次更新
- 提高性能和一致性
13.8 ref 和 reactive 的底层区别
答:
reactive基于 Proxy 代理对象ref通过.value做依赖收集和触发ref更适合包装基本类型
13.9 面试回答总模板
源码题建议按这个顺序答:
- 先说它解决什么问题
- 再说核心机制
- 再说执行流程
- 最后说收益和代价
14. 8 小时源码学习规划
第 1 小时:搭地图
目标:
- 搞懂
reactivity、runtime-core、compiler-core - 建立 template 到 DOM 的主流程
第 2-3 小时:响应式系统
目标:
- 搞清楚
track/trigger - 理解 effect
- 理解
ref/reactive/computed/watch
必须会:
- 为什么 Vue 3 用 Proxy
- computed 为什么有缓存
第 4 小时:编译器
目标:
- 理解 parse/transform/generate
- 理解 AST
- 理解 patch flags
第 5 小时:渲染器与 patch
目标:
- 理解 vnode
- 理解挂载与更新
- 理解 patch 主流程
第 6 小时:Diff
目标:
- 掌握 key 作用
- 掌握头尾比较
- 理解 LIS 的意义
第 7 小时:调度与 nextTick
目标:
- 理解 job 队列
- 理解异步更新
- 理解 nextTick
第 8 小时:复盘 + 面试表达
建议:
- 自己画出响应式依赖图
- 自己讲清 render 到 patch
- 自己讲清 Diff 和 nextTick
- 自己讲清 Vue 3 核心升级点
15. 一页速记总结
15.1 响应式
get时trackset时trigger- effect 是依赖重新执行的载体
15.2 编译
- parse
- transform
- generate
15.3 渲染
- render 产出 vnode
- patch 负责挂载和更新
15.4 Diff
- key 定身份
- 头尾优化
- 中间乱序用映射和 LIS
15.5 调度
- 更新先入队
- 队列去重
- 微任务统一刷新
nextTick等 DOM 更新完成
15.6 Vue 3 升级点
- Proxy 响应式
- Composition API
- 编译优化
- 更好的 TS 支持
16. 背诵口诀
16.1 响应式口诀
读就收集,写就触发;effect 负责重跑。
16.2 编译口诀
模板先成 AST,转换做优化,最后产出 render。
16.3 Diff 口诀
key 定身份,头尾先比较,中间看映射,LIS 减少移动。
16.4 调度口诀
更新先入队,统一再刷新,nextTick 等这一轮 DOM 刷完。
17. 源码阅读路线
17.1 第一阶段:只看概念主线
先回答:
- Vue 如何响应式
- 模板如何变成 render
- vnode 如何 patch 到 DOM
- 为什么 DOM 更新不是同步立刻执行
17.2 第二阶段:按模块看
推荐顺序:
- 先看
reactivity - 再看
compiler-core - 再看
runtime-core - 最后看调度和 patch 优化
17.3 第三阶段:带问题看源码
不要问“这一行干嘛”,而要问:
- 它属于哪个阶段
- 它解决哪个问题
- 如果没有它会怎样
17.4 阅读时最值得记录的内容
- 核心数据结构
- 关键流程函数
- 编译和运行时边界
- 高频面试表达模板
17.5 最后给你的建议
学 Vue 源码时,最重要的不是把每个细节都背下来,而是把这 5 条线讲顺:
- 为什么 Vue 3 用 Proxy
- 响应式如何 track/trigger
- 模板如何编译成 render
- vnode 如何 patch 到 DOM
- 为什么更新是异步批量的
只要你能把这 5 条线讲清楚,Vue 原理面试就已经很稳了。