Vue3 面试常从 API 用法切到源码原理:响应式如何追踪依赖、组件如何更新、虚拟 DOM 如何 diff、编译器做了什么优化。

响应式系统

Proxy、track 与 trigger

知识点讲解

Vue3 用 Proxy 拦截对象读取和修改。读取时通过 track 收集当前副作用函数,修改时通过 trigger 通知相关副作用重新执行。依赖关系通常可以理解成 target -> key -> effects

面试常问

问:Vue3 为什么用 Proxy 替代 Object.defineProperty?

答:Proxy 能拦截新增、删除、数组索引、Map/Set 等更多操作,不需要递归劫持所有属性,能力更完整。

ref、reactive 与 computed

知识点讲解

reactive 用于对象响应式,ref 用于包装任意值,模板里会自动解包。computed 有缓存,只有依赖变化才重新计算,适合派生状态。

面试常问

问:computed 和 watch 区别?

答:computed 用来声明派生值,有缓存,强调“算出一个值”;watch 用来监听变化并执行副作用,比如请求接口、操作缓存。

组件系统

props、emit 与 slot

知识点讲解

props 是父传子,emit 是子通知父,slot 是内容分发。组件设计时要保持单向数据流,子组件不要直接修改父组件传入的 props。

面试常问

问:为什么不能直接修改 props?

答:props 来源于父组件,直接修改会破坏单向数据流,也可能在父组件重新渲染时被覆盖。应该通过 emit 通知父组件修改。

provide/inject

知识点讲解

provide/inject 适合跨层级传递依赖,比如主题、表单上下文、组件库内部状态。它不适合替代全局状态管理,因为来源不如 store 清晰。

面试常问

问:provide/inject 是响应式的吗?

答:如果 provide 的是响应式对象或 ref,inject 后仍然保持响应式;如果提供普通值,则不会自动变成响应式。

渲染与更新

虚拟 DOM

知识点讲解

虚拟 DOM 是用 JS 对象描述真实 DOM。状态变化后生成新的 vnode,再通过 diff 找出最小必要更新。它带来跨平台能力和声明式渲染模型。

面试常问

问:虚拟 DOM 一定比原生 DOM 快吗?

答:不一定。虚拟 DOM 的优势是可维护性、跨平台和批量更新策略。手写极致优化 DOM 可能更快,但工程复杂度更高。

Diff 与 key

知识点讲解

Vue3 对同层节点做 diff。列表更新时 key 用来标识节点身份,帮助复用 DOM 和组件实例。没有稳定 key 时,可能导致状态错乱或更新低效。

面试常问

问:为什么不建议用 index 做 key?

答:列表插入、删除、排序时 index 会变化,Vue 可能错误复用节点,导致输入框状态、组件内部状态错乱。稳定唯一 id 更合适。

编译器与生态

template compiler

知识点讲解

Vue 模板会被编译成 render 函数。编译阶段可以做静态提升、patch flag、缓存事件处理函数等优化,减少运行时 diff 成本。

面试常问

问:patch flag 是什么?

答:它是编译器给动态节点打的标记,告诉运行时哪些部分可能变化。这样更新时不用全量比较,提高性能。

Pinia、Router 与 Nuxt

知识点讲解

Pinia 是 Vue 官方推荐状态管理,API 更轻、更贴近 Composition API。Vue Router 负责路由匹配和导航守卫。Nuxt 提供约定式路由、SSR、SSG、数据获取和部署能力。

面试常问

问:什么时候用 Pinia?

答:跨组件共享、需要持久化、需要 devtools 调试、多个页面共同依赖的状态适合放 Pinia。局部组件状态仍然用 refreactive

KeepAlive、Teleport 与 SSR

知识点讲解

KeepAlive 缓存组件实例,适合 Tab 和列表详情返回;Teleport 把组件渲染到 DOM 其他位置,适合弹窗;SSR 在服务端生成 HTML,改善首屏和 SEO。

面试常问

问:SSR 要注意什么?

答:服务端没有 windowdocument,副作用要放到客户端生命周期里;还要注意状态隔离,避免不同请求共享同一份全局状态。

底层追问与代码示例

手写一个最小响应式系统

知识点讲解

Vue3 响应式的核心可以简化为三件事:读取时收集依赖,修改时触发依赖,副作用函数重新执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const bucket = new WeakMap()
let activeEffect = null

function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}

function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}

function trigger(target, key) {
const depsMap = bucket.get(target)
const deps = depsMap?.get(key)
deps?.forEach(fn => fn())
}

function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
}
})
}

const state = reactive({ count: 0 })
effect(() => {
console.log('count:', state.count)
})
state.count += 1

真实 Vue 还要处理嵌套 effect、调度器、依赖清理、computed 懒执行、数组和集合类型、只读代理等复杂情况。

computed 的缓存原理

知识点讲解

computed 本质是懒执行的 effect。依赖不变时返回缓存值;依赖变化时只标记 dirty,等下次读取再重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function computed(getter) {
let value
let dirty = true

const runner = () => {
value = getter()
dirty = false
}

return {
get value() {
if (dirty) runner()
return value
},
markDirty() {
dirty = true
}
}
}

面试时可以说明:真实 Vue 的 computed 会通过 scheduler 在依赖变化时触发 dirty 标记,并在被其他 effect 读取时继续建立依赖关系。

Diff 中 key 的底层意义

知识点讲解

key 不是为了消除 warning,而是给 vnode 一个稳定身份。没有 key 时框架只能按位置复用;有 key 时可以判断节点是否移动、删除、插入。

1
2
3
4
5
<template>
<div v-for="user in users" :key="user.id">
<input :value="user.name" />
</div>
</template>

如果使用 index 做 key:

1
2
3
<div v-for="(user, index) in users" :key="index">
<input :value="user.name" />
</div>

当列表头部插入新用户时,旧 DOM 会按位置复用,输入框内部状态可能错位。

面试常问

问:Vue3 Diff 为什么会提到最长递增子序列?

答:在有 key 的列表 diff 中,Vue 会找出新旧节点中可以复用且相对顺序不变的最长递增子序列。这部分节点不用移动,其他节点再移动或插入,从而减少 DOM 操作。

watch 的 flush 时机

知识点讲解

watch 默认在组件更新前后有调度时机差异,常见配置有 prepostsync

1
2
3
watch(source, callback, {
flush: 'post'
})

post 适合在 DOM 更新后读取 DOM 状态;sync 会同步触发,容易造成频繁执行,谨慎使用。