跳到主要内容

Vue 面经

核心考点

1. 响应式原理

  • Vue 2:Object.defineProperty 劫持属性 getter/setter;无法检测属性新增/删除,需用 Vue.set
  • Vue 3:Proxy 代理整个对象,天然支持新增/删除属性,性能更好

2. Composition API vs Options API

对比项Options APIComposition API
逻辑组织按选项分散按功能聚合
逻辑复用Mixins(易冲突)Composables(清晰)
TypeScript较弱完整支持
适合场景简单组件复杂/大型项目

3. Vue 3 重要新特性

  • Proxy 响应式:性能提升,支持 Map/Set/Array
  • Teleport:将组件渲染到 DOM 指定位置(如 Modal 挂到 body)
  • Fragments:组件可有多个根节点
  • setup() / script setup:更简洁的组合式写法
  • Suspense:异步组件加载的内置支持

4. 生命周期对比

Vue 2 Vue 3 (Composition)
beforeCreate → setup()
created → setup()
beforeMount → onBeforeMount
mounted → onMounted
beforeUpdate → onBeforeUpdate
updated → onUpdated
beforeDestroy → onBeforeUnmount
destroyed → onUnmounted

5. computed vs watch vs watchEffect

  • computed:依赖缓存,有返回值,用于派生状态
  • watch:明确监听某个源,可获取新旧值,支持异步操作
  • watchEffect:自动收集依赖,立即执行,适合副作用同步

6. 高频面试题

  • v-model 原理:语法糖,绑定 :modelValue + @update:modelValue
  • nextTick 原理:异步队列(Promise → MutationObserver → setTimeout),DOM 更新后回调
  • keep-alive:缓存组件实例,触发 activated / deactivated 生命周期
  • diff 算法:Vue3 采用最长递增子序列优化移动操作
  • 为什么 data 是函数:防止组件实例间共享同一引用

💡 推荐阅读 Vue 3 官方文档 深入理解设计思路

Vue 3 响应式源码深入

响应式系统源码解析

1. 核心数据结构

// 依赖收集的核心映射
type Dep = Set<ReactiveEffect> // 订阅该属性的 effect 集合
const targetMap = new WeakMap<object, Map<string, Dep>>()

// WeakMap<target, Map<key, Set<effect>>>
// WeakMap 保证 target 被 GC 时自动清理

2. reactive / ref 区别

reactiveref
接受类型对象/数组任意值
访问方式直接访问属性.value
实现Proxygetter/setter(对象内部用 reactive)
解构会丢失响应式toRefs() 保持响应式

3. track / trigger 流程

【读取属性】
get trap → track(target, key)
→ 找到当前 activeEffect
→ targetMap[target][key].add(activeEffect)

【修改属性】
set trap → trigger(target, key)
→ 取出 targetMap[target][key]
→ 遍历执行所有 effect

4. effect / computed / watch 关系

  • ReactiveEffect 是核心类,computed/watch 都基于它
  • computed:懒执行,有 _dirty 标记,依赖变化时只标记脏,访问时才重算
  • watch:立即执行 effect,对比新旧值后调用回调
  • watchEffect:直接执行 effect,自动收集内部响应式依赖

5. 响应式边界问题

// ❌ 解构丢失响应式
const { count } = reactive({ count: 0 })

// ✅ 用 toRefs
const state = reactive({ count: 0 })
const { count } = toRefs(state)
count.value++ // 仍然响应式

// ❌ 将响应式对象赋值给普通变量
let user = reactive({ name: 'A' })
user = { name: 'B' } // 丢失响应式!

// ✅ 修改属性而非替换对象
user.name = 'B'

6. shallowReactive / readonly / shallowReadonly

  • shallowReactive:只代理第一层,深层不响应
  • readonly:深层只读,set 时警告
  • markRaw:标记对象不被转为响应式(如第三方实例)

7. 高频面试题

  • Vue 3 为什么用 Proxy 替换 Object.defineProperty?
    • 支持新增/删除属性、数组下标修改
    • 懒代理嵌套对象(访问时才代理),性能更好
    • 支持 Map/Set/WeakMap/WeakSet
  • ref 内部为什么对对象用 reactive? 保持深层响应式
  • computed 为什么是懒计算? 避免不被读取时白白计算
  • watchEffect 和 watch 的 flush 时机? 默认 pre(DOM 更新前),post 在 DOM 更新后

Vue 生态(Pinia / Vue Router / Nuxt)

Vue 生态核心面经

1. Pinia(替代 Vuex)

核心概念

export const useUserStore = defineStore('user', () => {
const name = ref('Alice')
const double = computed(() => name.value.repeat(2))
async function fetchUser(id: string) {
name.value = await api.getUser(id)
}
return { name, double, fetchUser }
})
对比Vuex 4Pinia
TypeScript较弱完整支持
模块化namespace每个 store 独立
Mutations必须不需要,直接修改
DevTools支持支持(更好)
  • $patch:批量修改状态,触发一次订阅
  • $subscribe:监听 store 变化(类似 vuex 的 subscribe)
  • 插件系统:可扩展 store(如持久化插件 pinia-plugin-persistedstate)

2. Vue Router 4

导航守卫执行顺序

触发导航
→ 全局 beforeEach
→ 路由独享 beforeEnter
→ 组件 beforeRouteUpdate(复用组件)
→ 组件 beforeRouteEnter
→ 全局 afterEach
→ beforeRouteEnter 的 next(vm => ...) 回调

动态路由

router.addRoute({ path: '/admin', component: Admin })
router.removeRoute('admin') // 通过 name 删除
  • 路由懒加载component: () => import('./views/Home.vue')
  • 滚动行为scrollBehavior(to, from, savedPosition) 自定义
  • 路由元信息meta: { requiresAuth: true } 配合守卫做权限控制

3. Nuxt 3 核心

目录结构约定

pages/ → 自动生成路由
components/ → 自动导入组件
composables/ → 自动导入 composables
server/api/ → 服务端 API 路由
plugins/ → 客户端/服务端插件
middleware/ → 路由中间件

数据获取

// useFetch:SSR + CSR 通用,自动序列化
const { data } = await useFetch('/api/users')

// useAsyncData:更灵活的异步数据
const { data } = await useAsyncData('users', () => $fetch('/api/users'))
  • Nitro 引擎:Nuxt 3 服务端,支持多部署目标(Node/Vercel/CF Workers)
  • Island Architecture<NuxtIsland> 服务端渲染的交互组件
  • SEOuseHead / useSeoMeta 管理 meta 标签

4. VueUse

常用 composables:

  • useStorage — localStorage/sessionStorage 响应式绑定
  • useFetch — 简单请求封装
  • useIntersectionObserver — 懒加载/埋点
  • useEventListener — 自动清理的事件绑定
  • useVModel — 简化 v-model 实现

5. 高频面试题

  • Pinia 如何实现跨 store 通信? 在一个 store 中直接导入并使用另一个 store
  • Vue Router history 模式需要服务器配置什么? 所有路径返回 index.html,避免 404
  • Nuxt SSR 如何避免服务端/客户端数据不一致(hydration mismatch)? 避免在服务端使用 window/document,用 onMounted 隔离客户端逻辑
  • Vue 项目如何做权限控制? 路由 meta + 全局守卫 + 动态添加路由 + 指令(v-permission)

Vue 大型项目架构实战

Vue 大型项目设计

1. 目录结构设计

src/
api/ # API 层:请求封装、类型定义
modules/
user.ts
product.ts
request.ts # Axios 封装、拦截器
types.ts # 响应类型
assets/ # 静态资源
components/ # 通用组件
base/ # 基础组件(Button、Input、Modal)
business/ # 业务组件
composables/ # 可复用的 composition functions
directives/ # 自定义指令
layouts/ # 布局组件
router/ # 路由配置
modules/ # 按模块分割路由
guards.ts # 导航守卫
stores/ # Pinia stores
modules/
utils/ # 工具函数
views/ # 页面组件(对应路由)

2. 请求层设计

// request.ts:统一 Axios 封装
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

class Request {
private instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000
})

constructor() {
this.setupInterceptors()
}

private setupInterceptors() {
// 请求拦截:注入 token
this.instance.interceptors.request.use(config => {
const token = useAuthStore().token
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})

// 响应拦截:统一错误处理
this.instance.interceptors.response.use(
(res: AxiosResponse<ApiResponse>) => {
if (res.data.code !== 0) {
showError(res.data.message)
return Promise.reject(res.data)
}
return res.data.data
},
async error => {
if (error.response?.status === 401) {
await useAuthStore().refreshToken()
return this.instance(error.config) // 重试
}
return Promise.reject(error)
}
)
}

get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T>(url: string, data?: unknown): Promise<T> {
return this.instance.post(url, data)
}
}

export const request = new Request()

3. 权限系统设计

// 基于 RBAC 的权限体系
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const permissions = ref<string[]>([])
const roles = ref<string[]>([])

function hasPermission(perm: string) {
return permissions.value.includes(perm)
}

function hasRole(role: string) {
return roles.value.includes(role)
}

return { permissions, roles, hasPermission, hasRole }
})

// composables/usePermission.ts
export function usePermission(permission: string) {
const store = useAuthStore()
return computed(() => store.hasPermission(permission))
}

// 自定义指令 v-permission
app.directive('permission', {
mounted(el, binding) {
const store = useAuthStore()
if (!store.hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})

// 路由守卫
router.beforeEach(async (to, from) => {
const store = useAuthStore()
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.permission && !store.hasPermission(to.meta.permission)) {
return { name: '403' }
}
})

4. 动态路由(按权限加载)

// 登录后根据权限动态注册路由
async function setupDynamicRoutes(permissions: string[]) {
const allRoutes = import.meta.glob('./modules/*.ts', { eager: true })
const accessibleRoutes = filterRoutesByPermission(allRoutes, permissions)
accessibleRoutes.forEach(route => router.addRoute(route))
}

// 刷新页面后路由恢复(动态路由会在刷新后丢失)
router.beforeEach(async (to) => {
const store = useAuthStore()
if (store.isLoggedIn && !store.routesLoaded) {
await store.loadUserRoutes() // 重新加载路由
return to.fullPath // 重新导航到目标路由
}
})

5. 组件通信规范

父子通信:props + emits(类型安全)
跨层级:provide / inject + readonly 包裹
兄弟/跨页:Pinia store
复杂异步:组合 composables
// 类型安全的 provide/inject
const FormContextKey = Symbol() as InjectionKey<FormContext>

// 父组件
provide(FormContextKey, readonly(formContext))

// 子组件
const form = inject(FormContextKey)
if (!form) throw new Error('FormContext not provided')

6. 性能优化实践

// 1. 路由懒加载(分包)
const Dashboard = () => import('./views/Dashboard.vue')

// 2. 组件异步加载(大型业务组件)
const HeavyTable = defineAsyncComponent({
loader: () => import('./HeavyTable.vue'),
loadingComponent: Skeleton,
errorComponent: ErrorView,
delay: 200,
timeout: 3000
})

// 3. 长列表虚拟滚动
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
items,
{ itemHeight: 50, overscan: 10 }
)

// 4. 计算属性缓存敏感数据
const expensiveData = computed(() => {
return heavyCalculation(rawData.value)
})

// 5. shallowRef 减少深层响应式
const tableData = shallowRef<Row[]>([]) // 不对数组元素建立深层响应

7. 资深面试题

  • Vue 大型项目如何解决 TypeScript 类型推导问题?
    • Volar 插件提供组件 props 类型推导
    • defineProps<{ title: string }>() 泛型语法直接推导类型
    • API 响应类型用 Zod 做运行时校验 + 类型推导
  • Pinia 如何在 SSR 中避免状态污染?
    • 每次请求创建新的 Pinia 实例,不共享服务端状态
    • useStore(pinia) 传入请求绑定的 pinia 实例
  • 如何做 Vue 项目的国际化?
    • vue-i18n 管理翻译文件,按语言懒加载
    • 日期、数字用 Intl API 处理本地化格式
    • 配合 CI/CD 自动提取新增翻译 key

Vue 编译器原理深入

模板编译全流程

1. 编译三阶段

模板字符串
-> Parse(词法+语法分析)-> AST(抽象语法树)
-> Transform(优化转换)->PatchFlagJavaScript AST
-> Codegen(代码生成)-> render 函数字符串

2. Parse 阶段

  • 有限状态机解析 HTML:Data -> Tag Open -> Tag Name -> Attribute
  • 生成元素节点、文本节点、插值节点
// <div :class="cls">{{ msg }}</div> 解析后
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{ name: 'class', exp: { content: 'cls' } }],
children: [{
type: NodeTypes.INTERPOLATION,
content: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'msg' }
}]
}

3. Transform 阶段(核心优化)

静态提升(Static Hoisting)

// 模板:
// <ul>
// <li class="static">固定内容</li>
// <li>{{ item }}</li>
// </ul>

// 静态节点提升到 render 函数外
const _hoisted_1 = createVNode('li', { class: 'static' }, '固定内容')

function render() {
return createVNode('ul', null, [
_hoisted_1, // 直接复用,不重新创建
createVNode('li', null, ctx.item, PatchFlags.TEXT)
])
}

PatchFlag 靶向更新

// 标记节点哪部分是动态的
createVNode('div',
{ class: dynamicClass },
null,
PatchFlags.CLASS // 只有 class 是动态的,diff 时只比较 class
)

常见 PatchFlag 值:

  • TEXT = 1:动态文本
  • CLASS = 2:动态 class
  • STYLE = 4:动态 style
  • PROPS = 8:动态非 class/style 属性
  • CACHED = -1:静态节点,跳过所有 diff

Block Tree

  • 每个含动态节点的模板区域是一个 Block
  • Block 记录其内部所有动态子节点(dynamicChildren)
  • Diff 时只遍历 dynamicChildren,跳过静态节点
  • v-if / v-for 会创建新 Block,因为结构会变化

4. v-if / v-for 编译

// v-if 编译为三元表达式
// <div v-if="show">A</div><div v-else>B</div>
_ctx.show ? createVNode('div', null, 'A') : createVNode('div', null, 'B')

// v-for 编译为 renderList
// <li v-for="item in list" :key="item.id">{{ item.name }}</li>
renderList(_ctx.list, (item) =>
createVNode('li', { key: item.id }, toDisplayString(item.name), 1)
)

// v-model 编译为双向绑定
// <input v-model="val">
createVNode('input', {
value: _ctx.val,
onInput: $event => { _ctx.val = $event.target.value }
})

5. 事件缓存

// <button @click="handleClick">按钮</button>
// 编译后:handler 被缓存,避免每次渲染创建新函数
createVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
})
// 内联 handler 会被包裹后缓存

6. 编译时 vs 运行时

版本说明Bundle 大小
vue.runtime.esm仅运行时,无编译器小(推荐,Vite/Webpack 用)
vue.esm-bundler运行时 + 编译器大(CDN 直接使用时)
  • 使用构建工具时:模板在构建阶段编译,运行时不需要编译器
  • 直接 <script src> 时:需要运行时编译器

7. 资深面试题

  • Vue 3 编译优化为什么比 Vue 2 快?
    1. 静态提升:不变的节点不重复创建
    2. PatchFlag:diff 只比较变化的部分
    3. Block Tree:O(全树) -> O(动态节点数)
    4. 事件缓存:handler 不每次重新创建
  • 为什么 v-if 和 v-for 不能同时使用?
    • Vue 3 中 v-if 优先级高于 v-for,v-if 内无法访问 v-for 的变量
    • 解决:外层包一个 <template v-for> 或用 computed 过滤数据
  • 自定义指令如何在编译层面优化?
    • 通过 directiveTransforms 在 Transform 阶段处理
    • 可以将部分指令逻辑转为静态属性,避免运行时指令调用开销
  • 模板中 key 的作用是什么?编译层面如何处理?
    • 用于标识节点身份,相同 key 的节点会尝试复用
    • 编译时 key 被提取为 VNode 的第二个参数
    • v-for 中动态 key 会生成 KEYED_FRAGMENT,告知 diff 算法使用带 key 的策略

Vue 性能优化深度实战

Vue 性能优化完整指南

1. 组件渲染优化

v-once 与 v-memo

<!-- v-once:只渲染一次,永不更新 -->
<ExpensiveComponent v-once />

<!-- v-memo:依赖的值不变则跳过重渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
{{ item.name }}
</div>

<!-- 组合使用:大列表时非常有效 -->
<RecycleScroller v-memo="[item.id, item.selected]">
<ExpensiveListItem :item="item" />
</RecycleScroller>

计算属性 vs 方法

<script setup>
// computed: 只在依赖变化时重计算,结果被缓存
const sortedList = computed(() =>
[...props.list].sort((a, b) => a.name.localeCompare(b.name))
)

// method: 每次渲染都执行
const getSorted = () => [...props.list].sort(...)
</script>

shallowRef / shallowReactive

// 大列表用 shallowRef 避免深层响应式的开销
const bigList = shallowRef([])
// 更新时需触发更新
bigList.value = [...bigList.value, newItem]

// 威尔用 markRaw 标记不需要响应式的对象
const map = markRaw(new Map())

2. 列表渲染优化

虚拟滚动(vue-virtual-scroller)

<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="60"
key-field="id"
>
<template #default="{ item }">
<ListItem :item="item" />
</template>
</RecycleScroller>
</template>

首屏渲染优化

<!-- 分批渲染大列表,首屏先展示 -->
<script setup>
const visibleCount = ref(50)
const visibleList = computed(() => list.value.slice(0, visibleCount.value))

onMounted(() => {
// 首屏渲染完成后再加载其余
nextTick(() => {
setTimeout(() => {
visibleCount.value = list.value.length
}, 300)
})
})
</script>

3. 路由与分包优化

// 路由懒加载 + 马鼓向加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 分包
children: [
{
path: 'analytics',
component: () => import('./views/Analytics.vue')
}
]
}
]

// 预加载:导航预获取数据
router.beforeResolve(async to => {
if (to.meta.prefetch) {
await prefetchData(to.meta.prefetch)
}
})

分包命名

// webpackChunkName 命名 chunk
const UserProfile = () =>
import(/* webpackChunkName: "user" */ './views/UserProfile.vue')

// Vite 自动分包
// 所有动态导入自动分包

4. 缓存策略

keep-alive 深入

<template>
<!-- 路由级缓存 -->
<router-view v-slot="{ Component, route }">
<transition name="fade">
<keep-alive :include="cachedViews" :max="10">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>

<script setup>
// 按需加入缓存列表
const cachedViews = ref(['Dashboard', 'UserList'])

// 切换路由时动态添加
router.afterEach((to) => {
if (to.meta.keepAlive) {
cachedViews.value.push(to.name)
}
})
</script>

keep-alive 的生命周期鋈子

<script setup>
// 组件被激活(从缓存中自展示)
onActivated(() => {
fetchLatestData() // 重新获取数据
})

// 组件被缓存

onDeactivated(() => {
cleanupResources() // 清理定时器、套接字等
})
</script>

5. 网络请求优化

// 并行请求多个接口
const [user, config, permissions] = await Promise.all([
fetchUser(),
fetchConfig(),
fetchPermissions()
])

// 请求去重:将相同请求合并
const pendingRequests = new Map()

function deduplicatedFetch(url: string) {
if (pendingRequests.has(url)) {
return pendingRequests.get(url)
}
const promise = fetch(url).finally(() => {
pendingRequests.delete(url)
})
pendingRequests.set(url, promise)
return promise
}

// 请求预加载:Intersection Observer 视口内就加载
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
prefetchNextPage()
}
})
})
observer.observe(bottomRef.value)

6. 资深面试题

  • Vue 3 为什么比 Vue 2 快?
    • 编译器优化:静态提升 + PatchFlag + Block Tree
    • Proxy 响应式:懒代理,访问时才建立响应式
    • Tree-shaking 友好:只包含用到的模块
  • v-for 的 key 如何影响性能?
    • key 不变:全量更新,运行全量 diff
    • key 稳定:精确匹配节点,只更新变化的部分
    • key = index:列表尾部操作时没有差异,中间插入错误
  • nextTick 的使用场景?
    • 修改数据后,需访问更新后的 DOM
    • 测如分页切换后的滚动位置重置
    • 在 created 中操作 DOM(DOM 还未挂载)
  • 如何定位 Vue 应用中的内存泄漏?
    • 监听器在 beforeUnmount 中取消
    • 定时器、EventBus、WebSocket 在生命周期结束时清理
    • Vue DevTools 测量组件数量变化
    • 浏览器 Memory Profiler 截图对比