把 Vue 拆开,
看清每一根数据流。
这不是一份 API 速查表。它从架构分层讲到响应式原理,从虚拟 DOM 的数据结构讲到一次完整渲染的全链路。读完之后,面试官问到 Vue 的任何一层,你都能从原理答到实现。
01 / FOUNDATIONVue 是什么,解决什么问题
先建立心智模型:Vue 的本质是「一套把数据状态自动映射到 DOM 视图」的渐进式框架。
Vue 是一个用于构建用户界面的渐进式 JavaScript 框架。它的核心目标只有一句话:让你只管修改数据,视图自动更新。开发者不再手动操作 DOM(document.getElementById 那一套),而是声明「数据长什么样、视图就长什么样」,剩下的同步工作交给框架。
「渐进式」的意思是:你可以只用它的核心库做一个页面交互,也可以叠加路由(Vue Router)、状态管理(Pinia)、构建工具(Vite)组成一个完整的单页应用(SPA)。每一层都是可选的、可渐进引入的。
三大设计支柱
| 支柱 | 含义 | 面试关键词 |
|---|---|---|
| 声明式渲染 | 用模板描述「数据 → 视图」的映射关系,而非命令式地操作 DOM | declarative · template |
| 响应式 | 数据变化时,依赖它的视图自动重新渲染 | reactivity · Proxy · 依赖收集 |
| 组件化 | UI 拆成可复用、可组合、自包含状态的组件树 | component tree · 单向数据流 |
「Vue 是一个数据驱动 + 组件化的渐进式框架,核心是响应式系统负责追踪依赖、虚拟 DOM负责高效更新、编译器把模板转成渲染函数,三者协作完成『数据变 → 视图变』的闭环。」
02 / ARCHITECTURE整体架构是怎么设计的
Vue 3 采用 monorepo 分包设计。理解分层,是理解一切实现的前提。
Vue 3 的源码是一个 monorepo(用 pnpm workspace 管理多个 package),按职责清晰分层。这种设计的好处是:每一层可以独立测试、独立发布、按需替换。比如响应式系统 @vue/reactivity 可以脱离 Vue 单独使用。
各层职责拆解
| Package | 职责 | 核心导出 |
|---|---|---|
| @vue/reactivity | 响应式内核:追踪数据读取、在数据变化时触发副作用。完全独立,不依赖任何渲染逻辑 | reactive ref effect computed |
| runtime-core | 平台无关的运行时:组件实例管理、虚拟 DOM、diff 算法、生命周期调度 | createRenderer h defineComponent |
| runtime-dom | 浏览器平台的具体实现:真实 DOM 的增删改、属性/事件处理 | createApp render |
| compiler-core | 模板编译器内核:template → AST → transform → 渲染函数代码 | baseCompile |
| compiler-dom | DOM 平台特有的编译转换(如 v-html、事件修饰符) | compile |
| compiler-sfc | 单文件组件 .vue 的编译器,被 Vite/Webpack 插件调用 | parse compileScript |
1) 关注点分离——响应式可被 SolidJS 等借鉴、可单独 npm 安装;2) 跨平台——只要替换 runtime-dom 为 runtime-canvas/小程序渲染器,核心逻辑全复用(这就是 Weex、跨端框架的基础);3) 编译时优化——把能在编译期算好的事情(静态提升、PatchFlag)提前算好,运行时少干活。
编译时 vs 运行时
这是 Vue 架构最重要的二分法。编译时发生在打包阶段(开发者机器或构建服务器上),把 .vue 文件里的 <template> 转成 JavaScript 渲染函数;运行时发生在浏览器里,执行渲染函数、跑响应式、操作真实 DOM。
compiler-sfc → render 函数
+ setup 代码 → 运行时
runtime-dom → 真实 DOM
03 / CORE响应式系统:Vue 的心脏
这是面试被问最多、也最能区分水平的部分。彻底搞懂「依赖收集」与「派发更新」。
响应式系统要解决的问题是:当我修改 state.count 时,框架怎么知道「哪些地方用到了它」,从而精确地去更新它们? 答案是两个动作的循环——依赖收集(track)和派发更新(trigger)。
Vue 3 用 Proxy 实现
Vue 2 用 Object.defineProperty 劫持每个属性的 getter/setter,Vue 3 改用 ES6 Proxy 代理整个对象。Proxy 能拦截 13 种操作(读、写、删除、in 判断、遍历……),从根本上解决了 Vue 2 的两大痛点:无法检测属性新增/删除、无法检测数组下标修改。
// reactive 的极简实现原理
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key) // ① 依赖收集:记录"谁在读我"
const res = Reflect.get(obj, key, receiver)
// 嵌套对象懒代理:只有访问到才递归 reactive
return typeof res === 'object' ? reactive(res) : res
},
set(obj, key, value, receiver) {
const ok = Reflect.set(obj, key, value, receiver)
trigger(obj, key) // ② 派发更新:通知所有依赖重新执行
return ok
}
})
}
effect:副作用与依赖的纽带
「依赖」到底是什么?是一个 effect(副作用函数)。组件的渲染过程本身就被包裹成一个 effect。当 effect 运行时读取了某个响应式数据,这个 effect 就被收集为该数据的依赖。
let activeEffect = null // 全局指针:当前正在运行的 effect
function effect(fn) {
const e = () => {
activeEffect = e // 运行前把自己挂上去
fn() // 运行 fn → 触发 get → track 收集到 e
activeEffect = null
}
e()
}
// 依赖存储结构:WeakMap → Map → Set
const targetMap = new WeakMap()
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) // key → 依赖它的 effect 集合
}
function trigger(target, key) {
const dep = targetMap.get(target)?.get(key)
dep?.forEach(e => e()) // 重新运行所有依赖
}
依赖存储是面试必考的数据结构:WeakMap<target, Map<key, Set<effect>>>。用 WeakMap 是为了对象被回收时依赖能自动 GC;用 Set 是为了天然去重。一句话:对象 → 属性 → 依赖该属性的 effect 集合。
ref vs reactive
| reactive | ref | |
|---|---|---|
| 适用 | 对象 / 数组 | 基本类型,也可包对象 |
| 实现 | Proxy 代理整个对象 | 对象 { value } + getter/setter 拦截 .value |
| 访问 | 直接 state.count | 需 .value(模板里自动解包) |
| 解构 | 会丢失响应性(需 toRefs) | 解构后仍保持响应 |
// ref 为什么需要 .value?因为基本类型无法被 Proxy 代理
function ref(raw) {
const r = {
get value() { track(r, 'value'); return raw },
set value(v) { raw = v; trigger(r, 'value') }
}
return r
}
computed 与 watch
computed 是一个带缓存的 effect:依赖不变时直接返回缓存值(靠一个 dirty 标志位实现懒计算)。watch 则是显式监听某个数据源,在变化时执行回调,适合做异步、副作用操作。
04 / CORE虚拟 DOM 与 Diff 算法
响应式负责「知道要更新」,虚拟 DOM 负责「高效地更新」。
直接操作真实 DOM 很慢,频繁操作更慢。虚拟 DOM(VNode)是用普通 JS 对象描述真实 DOM 结构。数据变化时,先生成新的 VNode 树,与旧树做 diff(比对),算出最小变更集,再一次性 patch(打补丁)到真实 DOM。
VNode 的数据结构
// 一个 VNode 大致长这样
const vnode = {
type: 'div', // 标签名 / 组件对象 / Fragment / Text
props: { id: 'app', onClick: fn },
children: [ ... ], // 子 VNode 数组,或字符串文本
key: null, // diff 时复用节点的身份标识 ★
el: null, // 对应的真实 DOM 引用(patch 后回填)
shapeFlag: 9, // 位运算标记:是元素?组件?文本子节点?
patchFlag: 1, // ★ Vue3 编译期优化:标记哪部分是动态的
}
Diff 算法:双端比较 + 最长递增子序列
Vue 3 的 diff 核心思想:同层比较(不跨层级)、靠 key 识别节点身份。对子节点数组,先做头头、尾尾预处理快速命中,剩下乱序的中间部分,用 最长递增子序列(LIS)算法求出「无需移动的最大节点集合」,最小化 DOM 移动次数。
key 是 VNode 的「身份证」。没有 key(或用 index 当 key)时,diff 只能就地复用,导致:① 带状态的子组件/输入框错位;② 列表顺序变化时产生大量不必要的 DOM 更新。用稳定唯一的业务 id 当 key,diff 才能精确识别哪个节点真的移动了。
05 / CORE模板编译:从 template 到 render
Vue 的「魔法」很多发生在编译期。理解这一步,才懂 Vue3 性能为何碾压 Vue2。
你写的 <template> 浏览器看不懂,必须先编译成 渲染函数(返回 VNode 的 JS 函数)。编译分三步:
模板 → AST → ② transform
优化 AST → ③ generate
AST → 代码
- 解析(parse):把模板字符串扫描成 AST(抽象语法树),描述每个标签、属性、指令、插值表达式的结构。
- 转换(transform):遍历 AST,做编译期优化——静态提升、PatchFlag 标记、事件缓存、处理
v-if/v-for等指令。 - 生成(generate):把优化后的 AST 拼成
render函数源码字符串。
编译期三大优化(Vue3 性能关键)
| 优化 | 做什么 | 收益 |
|---|---|---|
| 静态提升 (hoistStatic) | 把永远不变的 VNode 提到 render 函数外,只创建一次 | 避免每次渲染重复创建静态节点 |
| PatchFlag | 给动态节点打标记(如「只有 class 是动态的」),diff 时只比对标记的部分 | diff 从「全量比对」变「靶向更新」 |
| Block Tree | 用 block 收集所有动态子节点到扁平数组,跳过静态节点的递归 | diff 复杂度与「动态节点数」相关,而非「总节点数」 |
// 模板: <div class="static">{{ msg }}</div>
// 编译后(简化):
const _hoisted = createVNode("div", { class: "static" }) // 静态部分提升
function render(_ctx) {
return createVNode("div", null,
toDisplayString(_ctx.msg),
1 /* TEXT — PatchFlag: 只有文本动态 */
)
}
06 / CORE组件实例与生命周期
组件是 Vue 应用的基本单元。理解实例的诞生到销毁全过程。
每个组件在运行时都会创建一个组件实例(internal instance),它持有该组件的所有状态:props、setup 返回值、渲染函数、生命周期钩子、依赖的 effect 等。父子组件实例构成一棵组件树。
生命周期全景
| Composition API | Options API | 时机 |
|---|---|---|
setup() | beforeCreate/created | 实例创建,响应式数据初始化 |
onBeforeMount | beforeMount | 挂载前,render 即将首次调用 |
onMounted | mounted | 已挂载,真实 DOM 可访问 ★发请求/操作 DOM |
onBeforeUpdate | beforeUpdate | 数据变了,DOM 还没更新 |
onUpdated | updated | DOM 已更新 |
onBeforeUnmount | beforeDestroy | 卸载前 ★清理定时器/事件 |
onUnmounted | destroyed | 已卸载 |
① setup 里没有 this;② 在 onMounted 才能拿到真实 DOM,setup 里拿不到;③ 定时器、addEventListener、WebSocket 一定要在 onBeforeUnmount 里清理,否则内存泄漏。
setup 与 Composition API
// <script setup> 是 setup 的语法糖,编译时自动 return
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function inc() { count.value++ }
onMounted(() => console.log('mounted'))
Composition API 解决了 Options API 的两个核心痛点:逻辑复用(用组合函数 composable 替代 mixin,无命名冲突)和逻辑组织(同一功能的代码聚在一起,而非分散在 data/methods/computed 里)。
07 / DATA STRUCTURES重要的数据结构盘点
面试问「Vue 里有哪些核心数据结构」,答这几个就够拉满。
| 结构 | 形态 | 作用 |
|---|---|---|
| 依赖映射 | WeakMap<obj, Map<key, Set<effect>>> | 响应式追踪:哪个属性被哪些 effect 依赖 |
| VNode | 含 type/props/children/key/el/patchFlag 的对象 | 虚拟 DOM 的节点描述 |
| VNode 树 | VNode 通过 children 嵌套形成的树 | 一次渲染的整体快照 |
| Block Tree | VNode + dynamicChildren 扁平数组 | diff 时跳过静态节点 |
| 组件实例 | 含 props/setupState/render/effect/钩子的对象 | 组件运行时状态容器 |
| AST | 嵌套节点树(element/text/interpolation) | 编译期模板的结构表示 |
| 调度队列 | 去重的 effect 数组 + Promise microtask | 异步批量更新,避免重复渲染 |
当你在一个事件里连续改 10 次数据,Vue 不会渲染 10 次。它把触发的渲染 effect 推入一个去重队列,在当前同步代码跑完后、通过 Promise.then 创建的 microtask 里一次性 flush。这就是 nextTick 的原理——它本质是「在 DOM 更新后执行回调」。
08 / DATA FLOW数据流是怎么串起来的
把前面所有零件拼成一条完整的链路——这是面试的「升华题」。
一次完整渲染的全链路
把链路用语言串起来(这段建议背下来):
编译器把模板转成 render 函数。组件初始化时,setup 创建响应式数据。框架把「render 函数」包装成一个渲染 effect 并执行它——执行过程中读取响应式数据,触发 getter,把这个渲染 effect 收集为依赖。render 产出 VNode 树,经 patch 转成真实 DOM 挂载,触发 onMounted。
之后用户操作修改数据,触发 setter 的 trigger,找到依赖该数据的渲染 effect,推入异步队列。当前同步任务结束后在 microtask 里 flush,重新执行渲染 effect → 生成新 VNode 树 → 与旧树 diff → 计算最小变更 → patch 更新真实 DOM。闭环完成。
组件间的数据流向
Vue 强调单向数据流:父组件通过 props 把数据传给子组件(向下),子组件通过 emit 触发事件通知父组件(向上),父组件再修改数据。子组件不能直接修改 props。
- props / emit:父子通信,最标准
- v-model:props + emit 的语法糖,实现双向绑定
- provide / inject:跨层级(祖→孙)依赖注入
- Pinia / Vuex:全局状态,任意组件共享
- ref / expose:父访问子组件实例方法
09 / IN PRODUCTION实际项目里是怎么运行的
从 npm create 到浏览器渲染,一个真实 Vue 项目的完整运转。
项目结构与构建
现代 Vue 项目用 Vite 构建。开发时 Vite 利用浏览器原生 ESM 做免打包的按需编译(启动极快);生产时用 Rollup 打包。.vue 单文件组件由 @vitejs/plugin-vue 调用 compiler-sfc 编译成 JS。
my-app/
├─ index.html # 入口 HTML,挂载点 <div id="app">
├─ vite.config.js # 构建配置
├─ src/
│ ├─ main.js # 应用入口:createApp
│ ├─ App.vue # 根组件
│ ├─ router/ # Vue Router 路由表
│ ├─ stores/ # Pinia 状态
│ ├─ components/ # 可复用组件
│ ├─ views/ # 页面级组件
│ └─ composables/ # 组合式逻辑复用函数
└─ package.json
// src/main.js — 应用如何启动
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
createApp(App) // 创建应用实例
.use(createPinia()) // 装载状态管理插件
.use(router) // 装载路由插件
.mount('#app') // 挂载到 DOM,触发首次渲染
一个真实组件长什么样
<template>
<div class="todo">
<input v-model="text" @keyup.enter="add" />
<ul>
<li v-for="t in list" :key="t.id">{{ t.name }}</li>
</ul>
<p>共 {{ count }} 项</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const text = ref('')
const list = ref([])
const count = computed(() => list.value.length)
function add() {
list.value.push({ id: Date.now(), name: text.value })
text.value = ''
}
</script>
10 / ECOSYSTEM生态:路由与状态管理
真实项目离不开这两块。面试也常问。
Vue Router
SPA 的核心:URL 变化时切换组件而不刷新页面。两种模式:history(干净 URL,需服务端配合)和 hash(带 #,无需服务端)。核心概念:路由表、<router-view> 出口、<router-link>、动态路由、嵌套路由、导航守卫(权限拦截)。
Pinia(Vue 3 官方状态管理)
Pinia 取代了 Vuex,更简洁、TS 友好、无 mutations。核心三件套:state(状态)、getters(派生,类似 computed)、actions(修改状态的方法,可异步)。
import { defineStore } from 'pinia'
export const useCounter = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function inc() { count.value++ }
return { count, double, inc }
})
Vuex 改 state 必须走 mutation(同步)+ action(异步)两层;Pinia 直接在 action 里改,去掉了 mutation,API 更扁平,且对 TypeScript 类型推导更好。Vue 3 新项目一律推荐 Pinia。
11 / PERFORMANCE性能优化清单
面试问「你做过哪些 Vue 性能优化」,这些点能答满。
- v-for 加稳定 key——让 diff 精确复用
- v-if vs v-show——频繁切换用 v-show(只切 display),条件少变用 v-if(销毁重建)
- 路由懒加载——
() => import('./View.vue'),按需加载,减小首屏包体积 - 组件懒加载 + Suspense——
defineAsyncComponent - v-memo / shallowRef——跳过不必要的 diff / 浅层响应减少代理开销
- 大列表虚拟滚动——只渲染可视区节点
- computed 缓存——避免模板里写复杂表达式
- 合理的组件拆分——更新粒度更细,diff 范围更小
- keep-alive——缓存组件实例,避免重复渲染
- 事件解绑、定时器清理——防内存泄漏
12 / COMPARISONVue2 vs Vue3 vs React
对比题几乎必考。把差异点条理化记住。
Vue 2 → Vue 3 的关键升级
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy(解决数组/新增属性检测) |
| API 风格 | Options API | Composition API(+ Options) |
| 根节点 | 模板必须单根 | 支持多根(Fragment) |
| 性能 | 全量 diff | 静态提升 + PatchFlag + Block Tree |
| 体积/TS | 较大、TS 支持弱 | Tree-shaking 友好、TS 重写 |
| 新特性 | — | Teleport、Suspense、多 v-model |
Vue vs React
| 维度 | Vue | React |
|---|---|---|
| 更新机制 | 响应式自动追踪,靶向更新 | setState 触发,默认重渲染整棵子树(靠 memo 优化) |
| 模板 | template(可编译期优化) | JSX(更灵活,运行时为主) |
| 双向绑定 | v-model 内置 | 需手动 value + onChange |
| 心智 | 框架帮你做更多(约定优于配置) | 更接近 JS,自由度高 |
13 / INTERVIEW高频面试题速答
浓缩版问答。能流畅复述这些,90% 的 Vue 面试问题都能接住。
WeakMap→Map→Set 结构),在 setter 里 trigger 派发更新(取出依赖的 effect 重新执行)。组件渲染本身就是一个 effect。Vue.set 等 hack。Proxy 代理整个对象、拦截 13 种操作,从根本上解决,且懒代理嵌套对象,性能更好。Promise.then 的 microtask 一次性 flush。nextTick(cb) 就是把 cb 也放到这个 microtask 队尾,从而保证 cb 执行时 DOM 已更新。display(始终渲染,切换开销低)。频繁切换用 v-show,运行时很少变用 v-if。dynamicChildren 收集动态节点,diff 跳过静态部分。让运行时开销与「动态节点数」相关而非总节点数。activated/deactivated 钩子而非 mounted/unmounted。能讲清这五条,基本稳了:1) 响应式 = Proxy + track/trigger + WeakMap三层依赖;2) 渲染 = 模板编译成 render → 包成 effect → 产 VNode → patch;3) 更新 = trigger → 异步队列 → diff(双端+LIS)→ patch;4) 架构 = 编译层/运行时层/响应式层分包;5) Vue3 优势 = Proxy + Composition API + 编译期三大优化。