Skip to content

Vue

说明

共 15 题,难度 ⭐ ~ ⭐⭐⭐,覆盖 Vue 3 响应式原理、Composition API、虚拟 DOM、编译优化、组件通信等面试高频知识点。侧重与 React 对比,适合同时掌握两个框架的 5 年经验前端。

1. Vue 3 的响应式原理?ProxydefineProperty 的区别? ⭐⭐⭐

对比 Vue 2 和 Vue 3 的响应式实现方式。

考察点:Proxy、依赖收集、派发更新

Vue 2:Object.defineProperty

javascript
function defineReactive(obj, key, val) {
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend() // 收集当前 watcher
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify() // 通知所有 watcher
    }
  })
}

// 缺点:
// ❌ 无法检测属性的新增和删除 → 需要 Vue.set / Vue.delete
// ❌ 无法检测数组下标直接赋值 → arr[0] = 'x' 不触发更新
// ❌ 需要递归遍历所有属性,初始化性能差

Vue 3:Proxy

typescript
function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      const result = Reflect.get(target, key, receiver)
      if (typeof result === 'object' && result !== null) {
        return reactive(result) // 惰性递归代理
      }
      return result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key) // 派发更新
      }
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key) // 删除也能检测
      return result
    }
  })
}

对比

维度defineProperty (Vue 2)Proxy (Vue 3)
检测范围已有属性的 get/set所有操作(get/set/delete/has/...)
新增属性❌ 需要 Vue.set✅ 自动检测
删除属性❌ 需要 Vue.delete✅ 自动检测
数组操作❌ 需要重写数组方法✅ 原生支持
初始化递归遍历所有属性(性能差)惰性代理(按需)
兼容性IE9+不支持 IE

依赖收集与派发更新

依赖收集:
  组件渲染 → 访问响应式数据 → 触发 get → track()
  → 将当前 effect(渲染函数)存入该属性的依赖集合

  targetMap: WeakMap<object, Map<key, Set<effect>>>
  {
    obj: {
      'name': [effect1, effect2],
      'age':  [effect3]
    }
  }

派发更新:
  修改数据 → 触发 set → trigger()
  → 从 targetMap 中找到该属性的所有 effect → 重新执行

追问延伸

  • refreactive 的区别?什么时候用哪个?
  • 为什么 Proxy 要配合 Reflect 使用?(保证 receiver 正确)
  • Vue 3 的 shallowRef / shallowReactive 的使用场景?

2. refreactive 的区别?为什么有两套 API? ⭐⭐

对比两种响应式 API 的设计意图和使用场景。

考察点:响应式 API 选择

核心区别

维度refreactive
接受类型任意类型(原始值 + 对象)仅对象类型
访问方式.value直接访问
模板中自动解包(不需要 .value直接使用
解构不丢失响应式❌ 解构后丢失响应式
替换整体ref.value = newObj❌ 不能整体替换
底层实现原始值用 getter/setter,对象内部用 reactiveProxy

为什么需要 ref

typescript
// Proxy 不能代理原始值
const count = reactive(0)  // ❌ 不行!Proxy 只能代理 object

// ref 用 { value: T } 包装原始值
const count = ref(0)       // ✅ 内部变成 { value: 0 }
count.value++              // 触发响应式

解构问题

typescript
const state = reactive({ count: 0, name: 'Alice' })

// ❌ 解构后失去响应式
let { count, name } = state
count++ // 修改的是局部变量,不会触发更新

// ✅ 用 toRefs 转换
const { count, name } = toRefs(state)
count.value++ // 触发更新,因为 count 现在是 ref

// ✅ 或者从一开始就用 ref
const count = ref(0)
const name = ref('Alice')

选择建议

用 ref:
  - 原始值(number, string, boolean)
  - 需要替换整个对象(如从 API 获取新数据)
  - 需要在组合函数中返回(可解构)
  - 模板 ref(DOM 引用)

用 reactive:
  - 复杂对象且不需要解构
  - 表单数据(多个字段组成的对象)
  - 类似 React 的 useState 管理一组相关状态

实际项目中: ref 用得更多(更灵活,更安全)

追问延伸

  • toRefstoRef 的区别?
  • 为什么在 <template> 中不需要 .value?(编译器自动处理)
  • isRef() / isReactive() / isProxy() 分别检查什么?

3. Vue 的 Diff 算法?双端对比是什么? ⭐⭐⭐

解释 Vue 3 的 Diff 算法和最长递增子序列优化。

考察点:虚拟 DOM、双端 Diff、LIS 优化

Vue 3 的 Diff 流程

Vue 3 使用的是"快速 Diff 算法",分 5 步:

第 1 步: 从头部开始比较,相同就 patch
第 2 步: 从尾部开始比较,相同就 patch
第 3 步: 头尾处理完后,新节点多出的 → 新增
第 4 步: 头尾处理完后,旧节点多出的 → 删除
第 5 步: 中间部分用 Map + 最长递增子序列(LIS)处理

步骤详解

旧: a b [c d e f] g h
新: a b [e c d i] g h

第 1 步 (头部): a b 相同 → patch ✅
第 2 步 (尾部): g h 相同 → patch ✅
第 3-4 步: 中间部分不同 → 进入第 5 步

第 5 步 (中间乱序部分):
  旧中间: c d e f
  新中间: e c d i

  ① 建立新节点的 key → index 映射: { e:0, c:1, d:2, i:3 }

  ② 遍历旧中间节点,在映射中查找:
     c → 在新中找到,位置 1
     d → 在新中找到,位置 2
     e → 在新中找到,位置 0
     f → 在新中没找到 → 删除

  ③ 得到旧→新的位置映射: [1, 2, 0, -1]
     LIS(最长递增子序列): [1, 2] → 对应 c, d 不需要移动

  ④ 最终操作:
     c → 不移动(在 LIS 中)
     d → 不移动(在 LIS 中)
     e → 移动到 c 前面
     i → 新增
     f → 删除

对比 React 和 Vue 的 Diff

维度ReactVue 3
算法单向遍历双端 + LIS
移动策略lastPlacedIndex(单向)最长递增子序列(最优)
效率某些场景移动多余移动操作最少
复杂度O(n)O(n) + O(n log n) LIS
经典例子: 把 [A B C] 变成 [C A B]

React (单向):
  C(旧位置2 >= lastPlaced0) → 不动, lastPlaced=2
  A(旧位置0 < lastPlaced2)  → 移动 A
  B(旧位置1 < lastPlaced2)  → 移动 B
  → 移动 2 次

Vue (双端+LIS):
  LIS = [A, B] (递增序列)
  只需要移动 C → 1 次

追问延伸

  • LIS(最长递增子序列)的算法复杂度?Vue 用的什么实现?(贪心 + 二分 O(n log n))
  • Vue 2 的双端 Diff 和 Vue 3 有什么区别?
  • 为什么 Vue 不用 React 的 Fiber 架构?(Vue 的编译优化让 Diff 更快,不需要中断)

4. Composition API vs Options API 的设计差异? ⭐⭐

对比两种 API 风格的优劣和选择时机。

考察点:组合式 API

Options API 的问题

export default {
  data() { return { name: '', age: 0, items: [] } },
  computed: { ... },
  methods: {
    fetchUser() { ... },    // 用户相关
    fetchItems() { ... },   // 列表相关
    handleSort() { ... },   // 列表相关
  },
  watch: {
    name() { ... },         // 用户相关
    items() { ... },        // 列表相关
  },
  mounted() {
    this.fetchUser()        // 用户相关
    this.fetchItems()       // 列表相关
  }
}

问题: 一个功能的代码分散在 data / methods / computed / watch / lifecycle 各处
→ "关注点分离"变成了"关注点碎片化"

Composition API 的解决

typescript
// 按功能组织代码
function useUser() {
  const name = ref('')
  const age = ref(0)

  async function fetchUser() { /* ... */ }
  watch(name, () => { /* ... */ })
  onMounted(() => fetchUser())

  return { name, age, fetchUser }
}

function useItemList() {
  const items = ref([])

  async function fetchItems() { /* ... */ }
  function handleSort() { /* ... */ }
  watch(items, () => { /* ... */ })
  onMounted(() => fetchItems())

  return { items, fetchItems, handleSort }
}

// 组件中组合使用
export default {
  setup() {
    const { name, age } = useUser()
    const { items, handleSort } = useItemList()
    return { name, age, items, handleSort }
  }
}

对比

维度Options APIComposition API
代码组织按选项类型(data/methods/...)按功能逻辑
逻辑复用Mixins(有缺陷)组合函数(composables)
TypeScript类型推导弱✅ 类型推导优秀
学习曲线低(有结构约束)中(灵活度高)
适用场景小型组件、初学者复杂组件、大型项目

类比 React Hooks

Vue Composition API ≈ React Hooks

ref()              ≈ useState()
reactive()         ≈ useState({...})
computed()         ≈ useMemo()
watch()            ≈ useEffect() + 依赖
onMounted()        ≈ useEffect(() => {}, [])
composables        ≈ custom hooks

关键区别:
  React: 每次渲染重新执行组件函数(闭包)
  Vue:   setup() 只执行一次(响应式引用)
  → Vue 没有"闭包陷阱"、没有"依赖数组"的心智负担

追问延伸

  • Mixins 有哪些问题?Composition API 如何解决?
  • <script setup> 语法糖的编译原理?
  • Vue 3 中 Options API 和 Composition API 能混用吗?(可以)

5. computedwatch 的区别?watchEffect 又是什么? ⭐⭐

对比 Vue 3 中三种响应式侦听方式。

考察点:响应式副作用

三者对比

维度computedwatchwatchEffect
用途派生状态监听变化并执行副作用自动追踪依赖的副作用
返回值✅ 有(计算结果)❌ 无❌ 无
依赖声明自动追踪显式指定自动追踪
惰性✅(用到才计算)✅(默认值变化才执行)❌(立即执行)
缓存✅(依赖不变不重算)

computed

typescript
const firstName = ref('John')
const lastName = ref('Doe')

// 只读 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可写 computed
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val: string) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

watch

typescript
const userId = ref(1)
const userData = ref(null)

// 监听单个 ref
watch(userId, async (newId, oldId) => {
  userData.value = await fetchUser(newId)
}, { immediate: true }) // immediate: 立即执行一次

// 监听多个源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`${oldFirst} → ${newFirst}`)
})

// 监听 reactive 对象的某个属性(需要 getter)
const state = reactive({ count: 0 })
watch(() => state.count, (newCount) => {
  console.log(newCount)
})

// deep watch(深层监听)
watch(state, (newState) => {
  // reactive 对象默认 deep
}, { deep: true })

watchEffect

typescript
// 自动追踪所有访问到的响应式数据
const stop = watchEffect((onCleanup) => {
  const data = fetchData(userId.value) // 自动追踪 userId

  onCleanup(() => {
    // 清理副作用(类似 React useEffect 的 return)
  })
})

// 手动停止
stop()

选择指南

需要派生值(有返回值)?
  → computed ✅

需要知道旧值和新值?
  → watch ✅

需要副作用 + 自动追踪依赖?
  → watchEffect ✅

需要控制执行时机(flush: 'post' / 'sync')?
  → watch / watchEffect 都支持

追问延伸

  • computed 的缓存什么时候会失效?(依赖变化时 dirty 标记)
  • watchPostEffectwatchSyncEffect 是什么?
  • Vue 3.5 的 onWatcherCleanuponCleanup 有什么区别?

6. Vue 3 的编译优化?Block Tree 和 PatchFlags ⭐⭐⭐

解释 Vue 3 模板编译时的静态分析优化。

考察点:编译优化、静态提升

Vue 3 做了哪些编译优化

Vue 2:  运行时 Diff 遍历整棵虚拟 DOM 树
Vue 3:  编译时标记动态节点,运行时只 Diff 动态部分

优化手段:
  ① PatchFlags(补丁标记)
  ② Block Tree(块树)
  ③ 静态提升(Static Hoisting)
  ④ 事件监听缓存
  ⑤ SSR 优化

PatchFlags

vue
<template>
  <div>
    <p>静态文本</p>                    <!-- 无标记 -->
    <p>{{ message }}</p>              <!-- TEXT = 1 -->
    <p :class="cls">文本</p>          <!-- CLASS = 2 -->
    <p :id="id" :class="cls">文本</p> <!-- PROPS = 8 -->
  </div>
</template>
javascript
// 编译后的渲染函数
function render() {
  return createBlock('div', null, [
    createVNode('p', null, '静态文本'),
    createVNode('p', null, ctx.message, 1 /* TEXT */),
    createVNode('p', { class: ctx.cls }, '文本', 2 /* CLASS */),
    createVNode('p', { id: ctx.id, class: ctx.cls }, '文本', 8 /* PROPS */, ['id', 'class']),
  ])
}

// Diff 时根据 PatchFlags 只比较必要部分:
// TEXT → 只比较 textContent
// CLASS → 只比较 class
// PROPS → 只比较指定的 props (id, class)

Block Tree

普通 Diff:  递归遍历整棵树
Block Tree: 收集动态节点到 dynamicChildren 数组,扁平化遍历

<div>                   Block Root
  <p>static</p>         跳过
  <p>static</p>         跳过
  <p>{{ msg }}</p>       → dynamicChildren[0]
  <div>
    <span>static</span> 跳过
    <span>{{ a }}</span> → dynamicChildren[1]
  </div>
</div>

Diff 时: 只遍历 dynamicChildren = [<p>{{ msg }}</p>, <span>{{ a }}</span>]
跳过所有静态节点!

静态提升

javascript
// 未优化: 每次渲染都创建
function render() {
  return createVNode('div', null, [
    createVNode('p', null, '静态文本'), // 每次 render 都创建新 VNode
    createVNode('p', null, ctx.message),
  ])
}

// 静态提升后: 静态 VNode 只创建一次
const _hoisted = createVNode('p', null, '静态文本')

function render() {
  return createVNode('div', null, [
    _hoisted, // 复用同一个 VNode 对象
    createVNode('p', null, ctx.message),
  ])
}

事件监听缓存

javascript
// 未缓存: 每次渲染创建新函数 → 子组件被迫更新
createVNode('button', { onClick: () => ctx.handleClick() })

// 缓存后: 复用同一个函数引用
createVNode('button', {
  onClick: cache[0] || (cache[0] = () => ctx.handleClick())
})
// 类似 React 的 useCallback,但 Vue 编译器自动处理

追问延伸

  • 为什么 React 不能做类似的编译优化?(JSX 太灵活,无法静态分析)
  • Vue Vapor Mode 是什么?它不使用虚拟 DOM?
  • 可以在 Vue Playground 中查看编译输出吗?(template-explorer.vuejs.org)

7. Vue 的组件通信有哪些方式? ⭐

全面梳理 Vue 3 中组件之间的数据传递方式。

考察点:组件通信

通信方式汇总

方式方向适用关系
props父 → 子直接父子
emit子 → 父直接父子
v-model父 ↔ 子直接父子(双向)
provide / inject祖先 → 后代跨层级
expose / ref父 → 子(命令式)直接父子
attrs / slots父 → 子透传
Event Bus任意 → 任意兄弟/跨层级(不推荐)
Pinia全局全局共享

核心方式示例

vue
<!-- 1. props + emit -->
<!-- Parent.vue -->
<Child :name="name" @update="handleUpdate" />

<!-- Child.vue -->
<script setup>
const props = defineProps<{ name: string }>()
const emit = defineEmits<{ update: [value: string] }>()
</script>

<!-- 2. v-model(语法糖) -->
<!-- Parent.vue -->
<Child v-model:title="title" v-model:content="content" />

<!-- Child.vue -->
<script setup>
const props = defineProps<{ title: string; content: string }>()
const emit = defineEmits<{
  'update:title': [value: string]
  'update:content': [value: string]
}>()
</script>

<!-- 3. provide / inject -->
<!-- Ancestor.vue -->
<script setup>
const theme = ref('dark')
provide('theme', theme) // 提供响应式 ref
</script>

<!-- DeepChild.vue -->
<script setup>
const theme = inject('theme') // 注入
</script>

<!-- 4. expose + ref -->
<!-- Parent.vue -->
<script setup>
const childRef = ref()
childRef.value?.doSomething() // 调用子组件暴露的方法
</script>
<template>
  <Child ref="childRef" />
</template>

<!-- Child.vue -->
<script setup>
function doSomething() { /* ... */ }
defineExpose({ doSomething })
</script>

追问延伸

  • defineModel()(Vue 3.4+)如何简化 v-model 的实现?
  • provide/inject 和 React Context 有什么异同?
  • 为什么不推荐 Event Bus?Pinia 如何替代?

8. Vue Router 的导航守卫?路由懒加载? ⭐⭐

说明 Vue Router 的守卫机制和代码分割。

考察点:路由

导航守卫的完整执行顺序

导航从 /a 到 /b:

1. 组件 A 的 beforeRouteLeave      (组件内守卫)
2. router.beforeEach               (全局前置守卫)
3. 组件 B 的 beforeRouteEnter      (组件内守卫,组件还没创建)
   / 复用组件的 beforeRouteUpdate
4. 路由配置的 beforeEnter          (路由独享守卫)
5. 解析异步路由组件
6. router.beforeResolve            (全局解析守卫)
7. 导航确认
8. router.afterEach                (全局后置守卫)
9. DOM 更新
10. 组件 B 的 beforeRouteEnter 的 next 回调

实际用法

typescript
const router = createRouter({
  routes: [
    {
      path: '/dashboard',
      component: () => import('./views/Dashboard.vue'), // 路由懒加载
      meta: { requiresAuth: true },
      beforeEnter: (to, from) => {
        // 路由独享守卫
      }
    }
  ]
})

// 全局前置守卫 — 权限控制
router.beforeEach((to, from) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

// Composition API 中使用
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    return window.confirm('有未保存的修改,确定离开?')
  }
})

路由懒加载 + 代码分割

typescript
// 自动按路由分割代码
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue'),
    // Vite 会自动将 Dashboard.vue 打包为独立 chunk
  },
  {
    path: '/settings',
    component: () => import('./views/Settings.vue'),
  }
]

// 命名 chunk(Webpack / Vite)
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')

追问延伸

  • router.beforeResolverouter.beforeEach 有什么区别?
  • 如何实现路由级别的 <KeepAlive> 缓存?
  • Vue Router 的 scrollBehavior 如何控制滚动位置?

9. nextTick 的原理?什么时候需要用? ⭐⭐

解释 Vue 的异步更新机制和 nextTick 的实现。

考察点:异步更新

Vue 的异步更新机制

修改数据 → 不立即更新 DOM → 将更新放入队列
→ 在同一事件循环中的所有修改合并
→ 微任务中统一执行 DOM 更新

state.count = 1
state.count = 2
state.count = 3
// 只触发一次 DOM 更新,最终 count = 3

nextTick 的作用

typescript
import { ref, nextTick } from 'vue'

const count = ref(0)
const divRef = ref<HTMLDivElement>()

async function increment() {
  count.value++

  // ❌ DOM 还没更新
  console.log(divRef.value?.textContent) // '0'(旧值)

  // ✅ 等待 DOM 更新后
  await nextTick()
  console.log(divRef.value?.textContent) // '1'(新值)
}

nextTick 的实现

typescript
// Vue 3 的 nextTick 基于 Promise.resolve()(微任务)
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null

function nextTick(fn) {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(fn) : p
}

// 异步更新的调度:
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
    // flushJobs 执行所有排队的组件更新
  }
}

使用场景

typescript
// ① 修改数据后需要立即操作 DOM
const list = ref([])
async function addItem() {
  list.value.push({ id: Date.now() })
  await nextTick()
  scrollToBottom() // 滚动到新添加的项
}

// ② 在 setup / onMounted 中需要确保 DOM 就绪
onMounted(async () => {
  await nextTick()
  initThirdPartyLibrary(containerRef.value)
})

// ③ 获取更新后的组件尺寸
async function expand() {
  isExpanded.value = true
  await nextTick()
  const height = contentRef.value?.offsetHeight // 展开后的高度
}

追问延伸

  • Vue 2 的 nextTick 和 Vue 3 有什么区别?(Vue 2 有降级策略: Promise → MutationObserver → setImmediate → setTimeout)
  • nextTick 和 React 的 flushSync 解决的是同一个问题吗?
  • 多次调用 nextTick 的执行顺序?(按调用顺序,都在同一个微任务中)

10. Pinia 和 Vuex 的核心区别? ⭐⭐

对比 Vue 官方推荐的两代状态管理方案。

考察点:状态管理

核心区别

维度Vuex 4Pinia
API 风格Options(mutations/actions)Composition / Options 可选
Mutations✅ 必须通过 mutation 修改❌ 移除,直接修改 state
模块化modules + namespaced独立 Store,自动命名空间
TypeScript类型推导差✅ 类型推导完美
体积~1KB(不含 devtools)~1KB
Devtools
SSR需要额外配置内置支持

Pinia 的基本用法

typescript
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Composition API 风格(推荐)
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  async function fetchCount() {
    const res = await fetch('/api/count')
    count.value = await res.json()
  }

  return { count, doubleCount, increment, fetchCount }
})

// Options API 风格
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() { this.count++ },
    async fetchCount() {
      this.count = await fetch('/api/count').then(r => r.json())
    },
  },
})
vue
<!-- 组件中使用 -->
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const store = useCounterStore()

// ❌ 直接解构会丢失响应式
const { count } = store

// ✅ 用 storeToRefs 保持响应式
const { count, doubleCount } = storeToRefs(store)

// actions 可以直接解构
const { increment } = store
</script>

为什么 Pinia 移除了 Mutations

Vuex:
  state → 只能通过 mutation 同步修改
  mutation 的意义: Devtools 追踪每次变更

Pinia:
  state → 可以直接修改
  → Pinia 内部通过 $patch 和 $subscribe 实现了追踪
  → Devtools 同样能追踪每次变更
  → 少了一层 mutation 的样板代码

Store 间互相调用

typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const authStore = useAuthStore() // 直接 import 另一个 store
  const name = ref('')

  function logout() {
    name.value = ''
    authStore.clearToken() // 调用其他 store 的 action
  }

  return { name, logout }
})

追问延伸

  • Pinia 的 $subscribe 和 Vuex 的 subscribe 有什么区别?
  • Pinia 插件系统怎么用?(持久化、日志等)
  • 对比 Pinia 和 Zustand 的 API 设计?

11. <KeepAlive> 的原理?LRU 缓存策略? ⭐⭐

解释 KeepAlive 组件的缓存机制和生命周期。

考察点:组件缓存、LRU

KeepAlive 做了什么

正常的组件切换:
  显示 A → 切到 B → A 被销毁(unmounted)→ 切回 A → A 重新创建

KeepAlive 包裹的组件:
  显示 A → 切到 B → A 被"冻结"(deactivated)→ 切回 A → A 被"激活"(activated)
  → A 的 DOM 和状态完整保留

基本用法

vue
<template>
  <!-- 基本用法 -->
  <KeepAlive>
    <component :is="currentTab" />
  </KeepAlive>

  <!-- include / exclude(字符串、正则、数组) -->
  <KeepAlive :include="['TabA', 'TabB']" :exclude="/TabC/">
    <component :is="currentTab" />
  </KeepAlive>

  <!-- max — 最多缓存 N 个组件(LRU 策略) -->
  <KeepAlive :max="5">
    <component :is="currentTab" />
  </KeepAlive>

  <!-- 配合 Vue Router -->
  <RouterView v-slot="{ Component }">
    <KeepAlive :include="cachedViews">
      <component :is="Component" />
    </KeepAlive>
  </RouterView>
</template>

生命周期

typescript
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 组件从缓存中被激活(进入页面)
  // 适合: 刷新数据、恢复定时器、恢复滚动位置
  fetchLatestData()
})

onDeactivated(() => {
  // 组件被缓存(离开页面)
  // 适合: 暂停定时器、取消请求
  clearInterval(timer)
})

LRU 缓存原理

KeepAlive 内部维护一个缓存 Map + keys Set:

cache: Map<key, VNode>
keys: Set<key>

当 max = 3 时:
  访问 A → cache: [A], keys: {A}
  访问 B → cache: [A, B], keys: {A, B}
  访问 C → cache: [A, B, C], keys: {A, B, C}
  访问 D → 超过 max!LRU 淘汰最久未使用的 A
           cache: [B, C, D], keys: {B, C, D}
  再访问 B → B 移到"最近使用"
           keys: {C, D, B}(B 变为最新)

追问延伸

  • <KeepAlive> 和 React 的 Offscreen(Activity)有什么异同?
  • 如何手动清除 <KeepAlive> 的缓存?(动态修改 include/exclude)
  • KeepAlive 组件缓存的 DOM 保存在哪里?(移入 document.createDocumentFragment

12. <Teleport><Suspense> 的使用场景? ⭐⭐

说明 Vue 3 新增的两个内置组件。

考察点:内置组件

<Teleport> — 传送门

vue
<!-- 问题: Modal 嵌套在组件树深处,z-index 和 overflow 受限 -->
<div class="parent" style="overflow: hidden;">
  <Modal />  <!-- 被父容器裁剪 -->
</div>

<!-- Teleport: 将 DOM 传送到指定位置 -->
<template>
  <button @click="showModal = true">打开</button>

  <Teleport to="body">
    <div v-if="showModal" class="modal-overlay">
      <div class="modal">
        <p>弹窗内容</p>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

<!-- 逻辑上属于当前组件(响应式/事件正常工作) -->
<!-- DOM 上渲染到 body 下(脱离父容器限制) -->
vue
<!-- 常见使用场景 -->
<!-- 1. Modal / Dialog -->
<Teleport to="body"><Modal /></Teleport>

<!-- 2. Toast / Notification -->
<Teleport to="#toast-container"><Toast /></Teleport>

<!-- 3. Tooltip / Popover -->
<Teleport to="body"><Tooltip /></Teleport>

<!-- 4. 条件禁用 -->
<Teleport to="body" :disabled="isMobile">
  <Sidebar />
</Teleport>

<Suspense> — 异步组件加载

vue
<template>
  <Suspense>
    <!-- 默认插槽: 异步组件 -->
    <template #default>
      <AsyncDashboard />
    </template>

    <!-- fallback 插槽: 加载中显示 -->
    <template #fallback>
      <LoadingSkeleton />
    </template>
  </Suspense>
</template>

<script setup>
// AsyncDashboard.vue — 异步 setup
const data = await fetch('/api/dashboard').then(r => r.json())
// setup 中有 await → 该组件变成异步组件
// Suspense 会等待它 resolve 后再显示
</script>

嵌套 Suspense

vue
<Suspense>
  <template #default>
    <div>
      <UserProfile />  <!-- 快速加载 -->

      <Suspense>
        <template #default>
          <HeavyChart />  <!-- 慢数据 -->
        </template>
        <template #fallback>
          <ChartSkeleton />
        </template>
      </Suspense>
    </div>
  </template>
  <template #fallback>
    <FullPageLoader />
  </template>
</Suspense>

对比 React 的同名组件

维度Vue <Suspense>React <Suspense>
触发方式async setup()throw Promise / React.lazy
状态实验性(Experimental)稳定
数据获取async setup 直接 await需要配合 use() 或库

追问延伸

  • <Teleport> 和 React 的 createPortal 有什么区别?
  • <Suspense>onPending / onResolve / onFallback 事件有什么用?
  • 多个 <Teleport> 可以传送到同一个目标吗?(可以,按顺序追加)

13. Vue 和 React 的核心设计差异? ⭐⭐⭐

从架构设计层面对比两个框架。

考察点:框架设计、编译时 vs 运行时

核心理念差异

维度VueReact
定位渐进式框架UI 库
响应式Proxy 细粒度追踪不可变数据 + 重渲染
模板编译型模板(SFC)JSX(运行时)
更新粒度组件级精确更新组件树自顶向下 diff
优化策略编译时优化(PatchFlags)运行时优化(memo/Fiber)
心智模型可变数据 + 自动依赖追踪不可变数据 + 显式声明依赖

更新机制对比

Vue:
  state.count = 1
  → Proxy 拦截 set
  → 找到依赖这个属性的组件
  → 精确更新这些组件
  → 不需要 shouldComponentUpdate / memo

React:
  setState(1)
  → 标记组件 dirty
  → 从该组件开始向下 diff 整棵子树
  → 所有子组件默认重渲染
  → 需要 React.memo / useMemo 手动优化

编译时 vs 运行时

Vue(编译时优化):
  模板 → 编译器 → 优化后的渲染函数
  ✅ PatchFlags、Block Tree、静态提升
  ✅ 运行时 Diff 更少的工作
  ❌ 模板表达力受限

React(运行时优化):
  JSX → createElement 调用 → 完整 Diff
  ✅ JSX 完全就是 JavaScript,极其灵活
  ❌ 需要 Fiber 架构处理大组件树
  ❌ 需要 React Compiler 弥补编译优化

状态管理思维

javascript
// Vue: 可变数据,直接修改
const state = reactive({ items: [] })
state.items.push(newItem)  // ✅ 直接修改,Vue 自动检测

// React: 不可变数据,创建新引用
const [items, setItems] = useState([])
setItems([...items, newItem])  // ✅ 创建新数组
// items.push(newItem)         // ❌ 不会触发更新

如何选择

选 Vue:
  - 团队对 Vue 更熟悉
  - 渐进式引入(CDN script 标签也能用)
  - 更低的心智负担(不用操心 memo/依赖数组)
  - 内置功能丰富(transition, keep-alive, v-model...)

选 React:
  - 生态更大(社区、第三方库、招聘市场)
  - JSX 的灵活性(复杂 UI 逻辑)
  - React Native 跨平台需求
  - Next.js / RSC 的前沿技术
  - 函数式编程风格偏好

追问延伸

  • Vue Vapor Mode 和 Svelte 的"无虚拟 DOM"有什么区别?
  • 为什么 React 需要 Fiber 而 Vue 不需要?(Vue 编译时优化 + 精确更新 → Diff 更快)
  • 两个框架的 SSR 方案(Nuxt vs Next)的核心差异?

14. Vue 3 的 <script setup> 编译做了什么? ⭐⭐

解析 <script setup> 语法糖的编译原理。

考察点:编译原理、DX

<script setup> 前后对比

vue
<!-- 不用 <script setup> -->
<script>
import { ref } from 'vue'
import MyChild from './MyChild.vue'

export default {
  components: { MyChild },
  props: { title: String },
  emits: ['update'],
  setup(props, { emit }) {
    const count = ref(0)
    function increment() { count.value++ }
    return { count, increment }
  }
}
</script>

<!-- 使用 <script setup> -->
<script setup>
import { ref } from 'vue'
import MyChild from './MyChild.vue'

const props = defineProps<{ title: string }>()
const emit = defineEmits<{ update: [value: string] }>()

const count = ref(0)
function increment() { count.value++ }
// 不需要 return,所有顶层绑定自动暴露给模板
// 不需要 components,import 的组件自动注册
</script>

编译器做了什么

javascript
// <script setup> 编译后的实际代码(简化)
import { ref, defineComponent } from 'vue'
import MyChild from './MyChild.vue'

export default defineComponent({
  props: { title: { type: String } },
  emits: ['update'],
  setup(__props, { expose, emit }) {
    const count = ref(0)
    function increment() { count.value++ }

    // 编译器自动收集所有顶层变量
    return {
      count,
      increment,
      MyChild, // 组件也被收集
    }
  }
})

编译器宏(Compiler Macros)

typescript
// 这些函数不需要 import,编译时会被替换
defineProps<{ title: string }>()      // → 编译为 props 选项
defineEmits<{ click: [e: Event] }>()  // → 编译为 emits 选项
defineExpose({ method })               // → 编译为 expose 调用
defineModel()                          // → 编译为 prop + emit (3.4+)
defineOptions({ inheritAttrs: false }) // → 编译为组件选项
defineSlots<{ default: { msg: string } }>() // → 类型声明
withDefaults(defineProps<Props>(), {   // → 默认值
  title: 'hello'
})

ref 在模板中的自动解包

vue
<script setup>
const count = ref(0)
</script>

<template>
  <!-- 模板中不需要 .value -->
  <p>{{ count }}</p>
  <!-- 编译为: _ctx.count.value(编译器自动添加 .value) -->
</template>

追问延伸

  • defineProps 为什么不需要 import?它在运行时存在吗?(纯编译时宏,编译后被移除)
  • <script setup> 和普通 <script> 可以共存吗?(可以,用于 inheritAttrs 等场景)
  • Vue Macros 插件扩展了哪些能力?

15. Vue 3 性能优化的手段有哪些? ⭐⭐

给出 Vue 3 项目性能优化的完整清单。

考察点:性能优化

渲染优化

vue
<!-- ① v-once — 只渲染一次 -->
<h1 v-once>{{ title }}</h1>

<!-- ② v-memo — 有条件的缓存(Vue 3.2+) -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
  <!-- 只有 item.selected 变化时才重渲染这个项 -->
  <HeavyComponent :data="item" />
</div>

<!-- ③ 合理使用 KeepAlive -->
<KeepAlive :max="10" :include="cachedViews">
  <component :is="currentView" />
</KeepAlive>

<!-- ④ 列表渲染使用 key -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>

响应式优化

typescript
// ① shallowRef / shallowReactive — 大数据只追踪顶层
const bigList = shallowRef<Item[]>([])
// 修改内部不触发更新,需要整体替换
bigList.value = [...bigList.value, newItem]

// ② markRaw — 标记为非响应式
import { markRaw } from 'vue'
const heavyLib = markRaw(new EChartsInstance())
// ECharts 实例不需要响应式追踪,避免性能开销

// ③ computed 缓存
const filtered = computed(() =>
  items.value.filter(i => i.active)
)
// 只有 items 变化才重新计算,模板多次引用不重复计算

组件优化

typescript
// ① 异步组件 — 按需加载
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: LoadingSkeleton,
  delay: 200,
  errorComponent: ErrorDisplay,
  timeout: 3000,
})

// ② 函数式组件(无状态的轻量组件)
function RenderItem(props: { item: Item }) {
  return h('div', props.item.name)
}

构建优化

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-lib': ['element-plus'],
        }
      }
    }
  }
})

优化清单汇总

分类手段效果
渲染v-once / v-memo减少不必要的重渲染
渲染<KeepAlive>避免频繁销毁/创建
渲染虚拟列表大列表只渲染可见区域
响应式shallowRef / markRaw减少 Proxy 代理深度
响应式computed 缓存避免重复计算
加载异步组件 / 路由懒加载减少首屏 JS 体积
加载Tree-shakingVue 3 全局 API 可 tree-shake
构建代码分割 / manualChunks并行下载、缓存利用
运行时v-show vs v-if频繁切换用 v-show

追问延伸

  • v-memo 和 React 的 React.memo 有什么区别?
  • Vue 3 的 Tree-shaking 是如何实现的?(全局 API 改为具名导出)
  • markRawObject.freeze 有什么区别?

用心学习,用代码说话 💻