面试官:Vue 的这些原理你了解吗?
原文以 Vue 2.6.11 为背景,串讲了 Vue 与 React 的区别、响应式、computed、nextTick、Vue Router、路由插件和模板编译。本文尽量保留原来的源码阅读路线,修正抓取成一行的代码、拼写错误、Vue 2/3 API 混用和“Vue 3 完美解决所有问题”等绝对表述,并补充 Vue 3.5、Vue Router 4/5 和 Nuxt 4 的现行说法。
一、Vue 和 React 有什么区别
面试时可以先给出一个不绝对化的回答:
Vue 是一个渐进式 JavaScript 框架,提供响应式状态、组件模型、模板/渲染机制和一套官方生态;React 更常被定位为 UI 库,核心负责组件和渲染,路由、状态和数据获取通常由社区生态补充。两者都支持声明式 UI、组件化和单向数据流,也都可以使用 JSX。
| 维度 | Vue 3 | React |
|---|---|---|
| 定位 | 渐进式框架,官方生态较完整 | UI 库,生态组合更自由 |
| 模板 | SFC template、render function、JSX | JSX 为主,也有其他编译方案 |
| 响应式 | ref、reactive、computed、watch | state、context、外部 store 和渲染机制 |
| 组合逻辑 | Composition API、composable | Hooks、custom hooks |
| 官方路由 | Vue Router 4/5 | React Router 等社区方案 |
| 官方状态方案 | Pinia(Vue 官方推荐生态) | 没有唯一官方全局状态库 |
| 编译优化 | patch flags、静态提升、Block Tree | JSX/编译器及 React 的渲染调度优化 |
“Vue 双向绑定、React 单向数据流”也不能作为完整区别。Vue 组件内部同样遵循父传子、子通过事件通知父的单向数据流;v-model 只是对 props 和 update: 事件协议的语法封装。React 也可以通过受控组件、context 和状态库构造不同的数据流。
Vue 3 的 Composition API 与 React Hooks 都能复用逻辑,但两者运行模型、生命周期注册、响应式追踪和规则并不相同。Vue 2.7 也提供了部分 Composition API 兼容能力,但新项目通常选择 Vue 3。
二、new Vue() 阶段做了什么
下面的流程属于 Vue 2.6/2.7 Options API 的源码模型:
new Vue(options)
↓
Vue.prototype._init(options)
↓
初始化生命周期、事件、渲染能力和依赖注入
↓
beforeCreate
↓
initState:props、methods、data、computed、watch
↓
created
↓
$mount
↓
beforeMount → render watcher → mounted
Vue 2 中,data 会被 Observer 转换为响应式对象,普通对象属性通过 Object.defineProperty 定义 getter/setter。这个过程不是“给所有数据简单加一个监听器”,还包含依赖收集、数组方法拦截、嵌套对象递归观察和 watcher 调度。
可以用下面的简化代码理解 Dep:
class Dep {
static target = null
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
// 使用快照,避免派发期间修改订阅数组影响遍历。
for (const watcher of this.subs.slice()) {
watcher.update()
}
}
}
Dep 负责保存依赖,Watcher 是 Vue 2 中执行渲染、computed 或用户 watch 的观察者。真实源码还会去重依赖、维护 target stack,并通过 scheduler 批量更新。
2.1 Vue 3 的入口不同
Vue 3 不再使用 new Vue():
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
createApp 创建应用上下文,插件通过 app.use() 安装,组件在 setup()/<script setup> 中声明响应式状态和生命周期。Vue 3 响应式系统使用 Proxy、track、trigger 和 ReactiveEffect 等机制,不能直接把 Vue 2 的 Observer/Dep/Watcher 类名套到 Vue 3 源码上。

三、Vue 的响应式原理
3.1 Vue 2:getter 收集,setter 派发
Vue 2 的简化响应式属性可以写成:
function defineReactive(target, key, value) {
const dep = new Dep()
let observedValue = value
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
dep.depend()
return observedValue
},
set(nextValue) {
if (Object.is(nextValue, observedValue)) return
observedValue = nextValue
// 真实实现还会 observe 新对象,并触发模块钩子。
dep.notify()
},
})
}
依赖收集和派发更新的过程是:
渲染 watcher 执行 render
↓ 读取响应式属性
getter → dep.depend() → 当前 watcher 加入 dep
↓
修改属性
setter → dep.notify()
↓
watcher.update() → 加入异步更新队列
↓
重新 render 和 patch
Vue 2 中常见三类 watcher:
- render watcher:负责组件渲染;
- computed watcher:惰性求值和缓存计算属性;
- user watcher:由
watch创建,执行用户回调。
响应式 API 的标准名称是 Object.defineProperty,代码中应保持拼写一致。
3.2 Vue 2 的限制
Object.defineProperty 可以拦截已经存在属性的读取和写入,但不能自动知道未来会新增哪些属性,因此 Vue 2 需要:
this.$set(this.form, 'newField', value)
// 或 Vue.set(this.form, 'newField', value)
数组直接通过下标赋值或修改 length 也不能触发 Vue 2 的响应式更新:
this.items[3] = item // 可能无法触发更新
this.items.length = 0 // 不建议依赖响应式
Vue 2 通过改写 push、pop、shift、unshift、splice、sort、reverse 七个变更方法来通知更新;它不是因为 JavaScript 完全无法观察数组,而是 Vue 2 选择了这种兼顾成本的实现。Vue 2 可以观察已经存在的数组元素内部对象属性,但不会自动为越界新索引建立响应式定义。
3.3 Vue 3:Proxy 和 Reflect
Vue 3 使用代理拦截对象级别的操作:
const state = reactive({
count: 0,
list: [],
})
state.count++
state.list[0] = 'new item'
state.list.length = 0
教学版的依赖追踪可以简化为:
const targetMap = new WeakMap()
let activeEffect
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
const dep = depsMap?.get(key)
dep?.forEach(effect => effect())
}
实际 Vue 3 还要处理数组 length、迭代器、Map/Set、嵌套代理、只读对象、浅层代理、scheduler 和递归触发等情况。
Reflect 常用于在 Proxy handler 中正确转发默认行为:
const raw = { count: 0 }
const proxy = new Proxy(raw, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (!Object.is(oldValue, value)) trigger(target, key)
return result
},
})
Proxy 也不是“完美解决所有响应式问题”:它无法代理已经取出的原始值,解构会改变依赖追踪方式,第三方非响应式对象仍然需要手动处理,SSR 和副作用也要遵守对应环境规则。

四、计算属性的原理
4.1 Vue 2 computed watcher
Vue 2 的 computed watcher 通常是 lazy 的:
- 创建 computed watcher,但初始不立即计算;
- 第一次读取 computed 时执行 getter;
- getter 读取的响应式数据成为它的依赖;
- 依赖变化时把
dirty设为true; - 下一次读取时重新计算并缓存;
- 其他渲染 watcher 读取 computed 时,computed 还会把依赖通知给外层 watcher。
因此计算属性适合纯派生值:
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
},
}
4.2 Vue 3 computed
Vue 3 的 computed() 仍然是惰性和缓存的 ref:
import { computed, ref } from 'vue'
const price = ref(10)
const count = ref(2)
const total = computed(() => price.value * count.value)
只有依赖变化且再次读取时才重新计算。不要把有副作用的请求、修改其他状态或随机值放进 computed getter;需要副作用时使用 watch 或 watchEffect。
五、nextTick 原理
5.1 它解决什么问题
Vue 的响应式状态变化不会同步地把 DOM 立即改好:
<script setup>
import { nextTick, ref } from 'vue'
const name = ref('before')
const element = ref<HTMLElement | null>(null)
async function update() {
name.value = 'after'
console.log(element.value?.textContent) // 可能还是 before
await nextTick()
console.log(element.value?.textContent) // after
}
</script>
<template>
<div ref="element">{{ name }}</div>
<button @click="update">更新</button>
</template>
nextTick 的作用是等待当前这轮 DOM 更新 flush 完成后再执行回调或继续 Promise。它不是“等待任意异步请求”,也不是浏览器原生 API。
5.2 Vue 2 的队列模型
Vue 2 中修改数据大致经过:
setter
→ dep.notify()
→ watcher.update()
→ queueWatcher()
→ nextTick(flushSchedulerQueue)
→ watcher.run()
→ vm._render()
→ vm._update()
→ patch
queueWatcher 会按 watcher id 去重和排序,避免同一个同步执行段中重复渲染。原文把“异步渲染”理解为 setTimeout,这不够准确:Vue 2 使用异步 scheduler 批处理,具体的回调调度会根据环境选择 Promise、MutationObserver、setImmediate 或 setTimeout。
Vue 2 中 nextTick 的核心结构可以简化为:
const callbacks = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice()
callbacks.length = 0
for (const callback of copies) callback()
}
let timerFunc
if (typeof Promise !== 'undefined') {
const resolved = Promise.resolve()
timerFunc = () => resolved.then(flushCallbacks)
} else if (typeof MutationObserver !== 'undefined') {
// 真实源码通过修改文本节点触发 MutationObserver。
timerFunc = () => setTimeout(flushCallbacks, 0)
} else {
timerFunc = () => setTimeout(flushCallbacks, 0)
}
function nextTick(callback) {
callbacks.push(callback)
if (!pending) {
pending = true
timerFunc()
}
}
这是帮助理解的伪代码,不是完整 Vue 2 源码;真实实现还处理异常、Promise 返回值、iOS 兼容和环境判断。
5.3 Vue 3 的 nextTick 和 scheduler
Vue 3 的实现核心更接近:
const resolvedPromise = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
function nextTick(callback?: () => void) {
const promise = currentFlushPromise || resolvedPromise
return callback ? promise.then(callback) : promise
}
组件更新 job 会进入 scheduler 队列,执行过程中还会处理 pre-flush 和 post-flush 回调。nextTick 等待的是当前 flush promise;如果当前没有待处理更新,它通常只是等待一个已解决 Promise 的后续微任务。
Vue 3 watcher 还支持:
watch(source, callback, { flush: 'pre' }) // 默认,组件更新前
watch(source, callback, { flush: 'post' }) // DOM 更新后
watch(source, callback, { flush: 'sync' }) // 同步触发,谨慎使用
flush: 'post' 对“状态变化后读取 DOM”的 watcher 往往比在回调中再手动调用 nextTick 更直接。
六、Vue 能不能同步渲染
Vue 2 有 Vue.config.async = false 和 watcher 内部 sync 等历史调试/配置路径,但它们不是普通业务代码的推荐方案,可能破坏批处理、增加重复渲染和引入难以定位的更新顺序问题。
Vue 3 没有对应的全局 Vue.config.async 公共 API。若确实需要同步执行某个 watcher,可以使用 flush: 'sync',但它容易在循环、批量数组变更和递归更新中造成大量执行:
watch(source, callback, { flush: 'sync' })
大多数场景应该依赖默认批处理,并在需要访问 DOM 时使用 nextTick 或 flush: 'post'。
七、Vue Router 的核心原理
路由可以理解为 URL 状态与组件树之间的映射。SPA 中切换路由通常不会重新请求整个 HTML 文档,而是由路由器监听 URL 变化、匹配路由记录、加载组件并更新当前路由状态。
7.1 Vue Router 3、4 和 5 的 API
Vue 2 常见写法:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
],
})
Vue 3 及 Vue Router 4/5 使用工厂函数:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./Home.vue') },
],
})
app.use(router)
Vue Router 4/5 需要把 history 实现作为 history 传入;Nuxt 4 通常使用文件系统路由,不需要在页面应用里手写完整 createRouter 配置。
7.2 路由模式
- hash:URL 中
#后的部分变化,通常不会发送给服务器,部署简单; - history:使用
pushState、replaceState和popstate,URL 更自然,但服务器必须把未知路径回退到应用入口; - memory:在测试、SSR 或没有浏览器地址栏的环境使用内存 history。
改变 history URL 不会自动请求完整页面,但刷新、直接访问深层路径和服务端回退配置仍然需要考虑。
7.3 导航守卫
现代 Vue Router 推荐用返回值决定导航:
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
router.beforeResolve(async (to) => {
if (to.meta.requiresCamera) {
await askForCameraPermission()
}
})
router.afterEach((to, from, failure) => {
if (!failure) sendPageView(to.fullPath)
})
beforeResolve 在异步路由组件和组件内守卫解析后、导航确认前调用,适合执行只有在确认能进入页面时才需要的操作。afterEach 不能取消导航,但可以接收导航失败信息。
Vue Router 3/4 仍支持带 next 的旧式守卫,但现代代码更推荐每条控制路径返回一次结果:
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) return '/login'
return true
})
7.4 完整导航过程
常见流程可以概括为:
- 导航被触发;
- 调用失活组件的
beforeRouteLeave; - 调用全局
beforeEach; - 调用复用组件的
beforeRouteUpdate; - 调用路由记录的
beforeEnter; - 解析异步路由组件;
- 调用被激活组件的
beforeRouteEnter; - 调用全局
beforeResolve; - 导航确认;
- 调用全局
afterEach; - 触发 DOM 更新;
- Vue Router 3 的
beforeRouteEnter回调在组件实例创建后执行。
Vue Router 4/5 中还可以在 Composition API 中使用 onBeforeRouteUpdate 和 onBeforeRouteLeave,并不要求组件必须是路由组件。
八、手写一个简化 Vue Router
手写路由只能帮助理解,不应替代 Vue Router 的完整实现。它至少需要保存当前 URL、匹配路由、监听浏览器事件和渲染组件。
8.1 Vue 2 插件安装
Vue 2 插件通过 Vue.use() 调用 install:
let Vue
class MiniRouter {
constructor(options = {}) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routeMap = this.createMap(this.routes)
this.current = this.getCurrentPath()
}
createMap(routes) {
return routes.reduce((map, route) => {
map[route.path] = route.component
return map
}, Object.create(null))
}
getCurrentPath() {
if (this.mode === 'history') {
return window.location.pathname
}
return window.location.hash.slice(1) || '/'
}
}
MiniRouter.install = function (VueConstructor) {
Vue = VueConstructor
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this
this._router = this.$options.router
} else {
this._routerRoot = this.$parent?._routerRoot
}
},
})
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot?._router
},
})
}
真实 Vue Router 还要实现响应式 current route、router-link、router-view、导航守卫、异步组件、滚动行为和错误处理。原文只给出了固定渲染“首页”的组件,这可以作为插件注入的第一步,但还不是可用路由器。
8.2 Vue 3 插件安装
Vue 3 不再修改全局 Vue 构造函数,而是通过应用实例安装:
const router = {
install(app) {
app.provide('router', router)
app.config.globalProperties.$router = router
},
}
app.use(router)
实际实现通常使用 shallowRef 保存 currentRoute,并在 pushState/popstate 后更新它;router-view 根据匹配记录渲染组件,router-link 使用 router.push 并阻止默认跳转。
九、Vue 模板编译原理
浏览器不能直接理解 Vue 的 {{ }}、v-if、v-for 等模板语法。使用构建工具时,SFC 通常在构建阶段被编译成 render function;运行时构建版本则可能包含模板编译器。
9.1 Vue 2 的经典三阶段
parse:模板字符串 → AST
optimize:标记静态节点
generate:AST → render 函数字符串
Vue 2 的静态节点优化可以让后续更新跳过确定不会改变的部分,但组件、插槽和动态指令仍然需要按运行时规则处理。
9.2 Vue 3 的编译流程
Vue 3 compiler-core 大致包含:
baseParse
→ transform / directive transforms
→ generate
→ render function
编译器会为运行时提供更多信息:
- 静态节点提升;
- patch flags 标记动态文本、class、style、props 等;
- Block Tree 收集动态子节点;
- 事件处理器和静态属性缓存;
- 对
v-if、v-for、插槽和组件生成对应运行时代码。
这就是为什么 Vue 3 的 VDOM 不能简单理解为“每次把完整树拿来做通用 Diff”:编译器会告诉 runtime 哪些地方可能变化。
十、面试总结
回答 Vue 原理时,建议先说清版本和边界:
- Vue 2 的响应式核心是
Observer、Dep、Watcher和Object.defineProperty; - Vue 3 使用
Proxy、track/trigger、ReactiveEffect和 scheduler; - computed 基于响应式依赖缓存,watch 用于副作用;
nextTick等待当前 DOM 更新 flush,不是等待任意异步任务;- Vue 2/3 的更新都经过 render 和 patch,但 Vue 3 有 compiler-informed 优化;
- Vue Router 3 使用
new VueRouter,4/5 使用createRouter; - hash/history 是 URL 与服务器回退策略不同的两种模式;
- 模板编译大致经历解析、转换/优化和代码生成;
- Vue 3 组件推荐 Composition API、Pinia、Vue Router 4/5,Nuxt 4 还提供服务端渲染和数据获取能力;
- 不要把“Vue 是双向绑定”“Proxy 解决一切问题”“VDOM 一定更快”当成完整答案。
参考资料
- Vue 3:Reactivity in Depth
- Vue 3:响应式 API
- Vue 3:
nextTick - Vue 3:Rendering Mechanism
- Vue 2:Reactivity in Depth
- Vue Router:Navigation Guards
- Vue Router:不同 history 模式
- Vue 3:编译器与运行时
- Nuxt 4:Routing
原文作者:程序员 Better。原文关于 Vue/React 对比、Vue 2 响应式、computed、nextTick、Vue Router 插件、导航守卫和模板编译的主线予以保留;抓取成行的源码、错误拼写、无效示例、Vue 2/3 混用和不准确的绝对结论已整理或修正。