Vue3 高频面试题

Vue3 高频面试题

1. Vue3 的响应式原理(Proxy)

Vue3 使用 Proxy 替代 Vue2 的 Object.defineProperty,实现了更强大、更高效的响应式系统。

核心:reactive(对象) + ref(基本类型) + effect(副作用追踪)

特性 Vue2(defineProperty) Vue3(Proxy)
检测属性添加/删除 ❌ 需要 $set/$delete ✅ 自动检测
数组下标修改 ❌ 需要 $set ✅ 自动检测
Map/Set/WeakMap
深度监听 递归遍历(初始化开销大) 惰性代理(访问时才代理)
性能 初始化慢 按需代理,更快
javascript 复制代码
// ========== Vue3 响应式核心原理(简化) ==========
const targetMap = new WeakMap()  // 全局依赖存储
let activeEffect = null          // 当前正在执行的副作用

// 依赖收集
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)
}

// 派发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) dep.forEach(effect => effect())
}

// reactive 实现
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      // ✅ 惰性代理:访问嵌套对象时才递归创建 Proxy
      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
    }
  })
}

// ref 实现(包装基本类型)
function ref(value) {
  const wrapper = {
    get value() {
      track(wrapper, 'value')
      return value
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal
        trigger(wrapper, 'value')
      }
    }
  }
  return wrapper
}

💡 面试加分点: Vue3 的"惰性代理"是性能提升的关键------只有访问嵌套对象时才创建 Proxy,而非像 Vue2 一样初始化时递归遍历。WeakMap 存储依赖关系,当对象被 GC 时依赖自动清理,不会内存泄漏。


2. ref 和 reactive 怎么选?

特性 ref reactive
适用类型 任意类型(基本类型 + 对象) 仅对象/数组
访问方式 .value(模板中自动解包) 直接访问属性
重新赋值 count.value = 10 ❌ 不能整体替换
解构 ✅ 不丢失响应式 解构丢失响应式
TS 推断 自动推断泛型 需要接口定义
javascript 复制代码
import { ref, reactive, toRefs, toRef, isRef, unref, shallowRef, triggerRef } from 'vue'

// ✅ ref:推荐作为默认选择
const count = ref(0)
const name = ref('张三')
const user = ref({ name: '张三', age: 25 })
user.value = { name: '李四', age: 30 }  // ✅ 可以整体替换

// ✅ reactive:复杂对象场景
const state = reactive({ count: 0, list: [], user: null })
state.count++              // 直接修改
state.list.push('item')   // 数组方法也是响应式的
// state = { count: 1 }   // ❌ 不能整体替换!

// ⚠️ reactive 解构会丢失响应式
const { count } = state           // ❌ count 不再是响应式的
const { count } = toRefs(state)   // ✅ toRefs 保持响应式
const countRef = toRef(state, 'count')  // ✅ 取单个属性的 ref

// ✅ shallowRef:大数据优化(只追踪 .value 变化,不递归代理内部)
const bigData = shallowRef({ list: new Array(10000) })
bigData.value.list[0] = 'new'  // ❌ 不会触发更新
bigData.value = { list: [...] }  // ✅ 替换 .value 才触发
triggerRef(bigData)  // ✅ 手动触发更新

// ✅ 实用工具
isRef(count)    // true
unref(count)    // 0(ref 返回 .value,否则直接返回)

💡 面试加分点: Vue 官方推荐统一用 refref 的优势:可整体替换、解构不丢失响应式、更易追踪(.value 访问)。shallowRef + triggerRef 是大数据场景的标准优化手段。


3. Composition API vs Options API

Composition API 是 Vue3 最重要的新特性,将逻辑按功能聚合而非按选项分散。

javascript 复制代码
// ❌ Options API 的问题:同一功能的代码分散在不同选项中
export default {
  data() {
    return {
      // 功能A的数据 和 功能B的数据 混在一起
      userList: [], userLoading: false,
      searchQuery: '', searchResults: []
    }
  },
  computed: { /* A和B的computed混在一起 */ },
  methods: { /* A和B的方法混在一起 */ },
  watch: { /* A和B的watch混在一起 */ }
}

// ✅ Composition API:逻辑按功能聚合,可提取为 composable
// composables/useUsers.ts
export function useUsers() {
  const list = ref([])
  const loading = ref(false)
  const fetchUsers = async () => {
    loading.value = true
    try { list.value = await api.getUsers() }
    finally { loading.value = false }
  }
  onMounted(fetchUsers)
  return { list, loading, fetchUsers }
}

// composables/useSearch.ts
export function useSearch(fetchFn) {
  const query = ref('')
  const results = ref([])
  const debouncedSearch = useDebounceFn(async () => {
    results.value = await fetchFn(query.value)
  }, 300)
  watch(query, debouncedSearch)
  return { query, results }
}

// 组件中组合使用
// <script setup>
const { list, loading, fetchUsers } = useUsers()
const { query, results } = useSearch(api.searchUsers)
// </script>

Composable 编写规范:

javascript 复制代码
// ✅ 完整的 composable 示例
// composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(toValue(url))
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => { toValue(url); execute() })  // url 变化自动重新请求

  return { data, error, loading, refetch: execute }
}

// 使用
const { data: users, loading, error } = useFetch<User[]>('/api/users')
// 响应式 URL(URL 变了自动重新请求)
const userId = ref(1)
const { data: user } = useFetch(() => `/api/users/${userId.value}`)

💡 面试加分点: Composable 命名以 use 开头,返回 ref 而非 reactive(方便解构)。Composable 可以接受 MaybeRefOrGetter 类型参数,用 toValue() 解包,实现既支持静态值又支持响应式值。


4. <script setup> 语法糖详解

html 复制代码
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

// ✅ 顶层变量/函数自动暴露给模板
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++

// ✅ 导入的组件自动注册
// 直接在模板中使用 <MyComponent />

// ✅ defineProps:声明 props(编译器宏,不需要 import)
interface Props {
  title: string
  count?: number
  user?: { name: string; age: number }
}
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  user: () => ({ name: '', age: 0 })
})

// ✅ defineEmits:声明事件
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()
// Vue 3.3+ 简化写法
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
}>()

// ✅ defineModel:双向绑定(Vue 3.4+)
const modelValue = defineModel<string>()
const name = defineModel<string>('name')

// ✅ defineExpose:暴露给父组件的属性/方法
const inputRef = ref<HTMLInputElement>()
const focus = () => inputRef.value?.focus()
defineExpose({ focus, count })

// ✅ defineOptions:声明组件选项(Vue 3.3+)
defineOptions({
  name: 'MyComponent',
  inheritAttrs: false
})

// ✅ defineSlots:声明插槽类型(Vue 3.3+)
const slots = defineSlots<{
  default(props: { msg: string }): any
  header(props: { title: string }): any
}>()

// ✅ useAttrs / useSlots
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs()
const slots2 = useSlots()
</script>

💡 面试加分点: <script setup> 中的 definePropsdefineEmitsdefineModeldefineExposedefineOptionsdefineSlots 都是编译器宏 ,不需要 import。Vue 3.3 新增了 defineOptionsdefineSlots,Vue 3.4 新增了 defineModel


5. Vue3 生命周期

scss 复制代码
setup()              ← 替代 beforeCreate + created
onBeforeMount()      ← beforeMount
onMounted()          ← mounted
onBeforeUpdate()     ← beforeUpdate
onUpdated()          ← updated
onBeforeUnmount()    ← beforeDestroy(Vue2)
onUnmounted()        ← destroyed(Vue2)
onActivated()        ← keep-alive 激活
onDeactivated()      ← keep-alive 停用
onErrorCaptured()    ← 捕获后代错误
onServerPrefetch()   ← SSR 专用
javascript 复制代码
// <script setup>
import {
  onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted, onActivated, onDeactivated,
  onErrorCaptured
} from 'vue'

// ✅ setup 本身 = beforeCreate + created
console.log('setup 执行(等同于 created)')
const data = ref(null)
fetchData().then(res => data.value = res)  // 在 setup 中发请求

onMounted(() => {
  console.log('DOM 挂载完成')
  // 初始化第三方库、操作 DOM、添加事件监听
  initChart()
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  // ✅ 清理副作用(避免内存泄漏)
  destroyChart()
  window.removeEventListener('resize', handleResize)
  clearInterval(timer)
})

onErrorCaptured((err, instance, info) => {
  console.error('子组件错误:', err)
  return false  // 阻止向上传播
})

// ✅ 同一个钩子可以注册多次(按顺序执行)
onMounted(() => console.log('mounted 回调 1'))
onMounted(() => console.log('mounted 回调 2'))

父子组件生命周期顺序(与 Vue2 相同):

arduino 复制代码
挂载:父setup → 父onBeforeMount → 子setup → 子onBeforeMount → 子onMounted → 父onMounted
更新:父onBeforeUpdate → 子onBeforeUpdate → 子onUpdated → 父onUpdated
卸载:父onBeforeUnmount → 子onBeforeUnmount → 子onUnmounted → 父onUnmounted

💡 面试加分点: setup() 中没有 this,所有组合式 API 的生命周期钩子可以注册多次且按顺序执行。onServerPrefetch 是 SSR 专用钩子,在服务端渲染时预取数据。


6. computed 与 watch / watchEffect

特性 computed watch watchEffect
依赖声明 自动追踪 显式指定 自动追踪
缓存
旧值
立即执行 访问时执行 immediate: true 默认立即
异步
用途 派生数据 监听变化做副作用 自动追踪多依赖
javascript 复制代码
import { ref, computed, watch, watchEffect, watchPostEffect } from 'vue'

const firstName = ref('张')
const lastName = ref('三')
const searchQuery = ref('')
const page = ref(1)

// ✅ computed:派生数据(有缓存)
const fullName = computed(() => `${firstName.value}${lastName.value}`)
// 可写 computed
const writableFullName = computed({
  get: () => `${firstName.value}${lastName.value}`,
  set: (val) => { firstName.value = val[0]; lastName.value = val.slice(1) }
})

// ✅ watch:显式监听,可获取旧值
watch(searchQuery, (newVal, oldVal) => {
  console.log(`${oldVal} → ${newVal}`)
  page.value = 1
})
// 监听多个源
watch([searchQuery, page], ([q, p], [oldQ, oldP]) => {
  fetchData(q, p)
})
// 选项
watch(source, callback, {
  immediate: true,  deep: true,
  flush: 'post',    // 'pre'(默认) | 'post'(DOM更新后) | 'sync'
  once: true,       // Vue 3.4+,只触发一次
})

// ✅ watchEffect:自动追踪 + 默认立即执行
const stop = watchEffect((onCleanup) => {
  const controller = new AbortController()
  fetch(`/api?q=${searchQuery.value}&p=${page.value}`, {
    signal: controller.signal
  }).then(r => r.json()).then(data => { /* ... */ })

  onCleanup(() => controller.abort())  // ✅ 清理上一次请求(处理竞态)
})
stop()  // 手动停止

// ✅ watchPostEffect:DOM 更新后执行(= watchEffect + flush: 'post')
watchPostEffect(() => {
  // 可以安全访问更新后的 DOM
})

💡 面试加分点: watchEffectonCleanup 是处理竞态条件 的标准方案------依赖变化重新执行前,先取消上一次未完成的请求。Vue 3.5+ 中 onCleanup 改名为 onWatcherCleanup,可从 vue 导入。

watch 与 watchEffect 的核心区别(面试重点):

区别点 watch watchEffect
依赖声明方式 显式指定监听源 自动收集回调中使用的响应式依赖
执行时机 默认惰性(变化才执行) 立即执行一次(收集依赖)
旧值访问 ✅ 回调第二个参数是旧值 ❌ 无法获取旧值
监听精度 可以精确指定某个 ref / 某个属性 回调中所有访问到的响应式数据都是依赖
深度监听 需要 { deep: true } 默认会追踪回调中所有嵌套访问
适用场景 需要旧值对比 / 精确控制 / 条件触发 多依赖自动追踪 / 副作用同步
javascript 复制代码
import { ref, reactive, watch, watchEffect } from 'vue'

const userId = ref(1)
const filters = reactive({ status: 'active', page: 1 })
const data = ref(null)

// ============ watch 适用场景 ============

// 场景1:需要旧值对比
watch(userId, (newId, oldId) => {
  console.log(`用户从 ${oldId} 切换到 ${newId}`)
  // 可以基于新旧值做不同处理
  if (oldId !== newId) {
    resetPageState()
  }
  fetchUser(newId)
})

// 场景2:精确监听对象中的某个属性(用 getter 函数)
watch(
  () => filters.status,  // ⚠️ 监听 reactive 对象的属性必须用 getter
  (newStatus) => {
    console.log('状态筛选变了:', newStatus)
    filters.page = 1  // 切换状态时重置页码
  }
)

// 场景3:监听整个 reactive 对象(自动 deep)
watch(filters, (newVal) => {
  // ⚠️ reactive 对象作为监听源时,自动深度监听
  // ⚠️ 此时 newVal === oldVal(同一个对象引用)
  fetchList(newVal)
})

// 场景4:只在特定条件下才执行操作
watch(userId, (id) => {
  if (id > 0) {  // 条件判断
    fetchUser(id)
  }
}, { immediate: true })

// 场景5:使用 once 只触发一次(Vue 3.4+)
watch(userId, (id) => {
  analytics.track('first_user_view', { id })
}, { once: true })


// ============ watchEffect 适用场景 ============

// 场景1:多依赖自动追踪(不用手动列出所有依赖)
watchEffect(async () => {
  // 自动追踪 userId.value、filters.status、filters.page
  const res = await fetch(
    `/api/users/${userId.value}?status=${filters.status}&page=${filters.page}`
  )
  data.value = await res.json()
  // 任何一个依赖变化都会重新执行
})

// 场景2:副作用与依赖绑定(类似 React useEffect 但不需要依赖数组)
watchEffect(() => {
  document.title = `用户 ${userId.value} - 第 ${filters.page} 页`
  // 不需要像 watch 那样手动列出 [userId, () => filters.page]
})

// 场景3:配合 onCleanup 处理竞态
watchEffect((onCleanup) => {
  const controller = new AbortController()

  fetch(`/api/users/${userId.value}`, { signal: controller.signal })
    .then(r => r.json())
    .then(d => { data.value = d })

  // 依赖变化时,先取消上一次请求
  onCleanup(() => controller.abort())
})


// ============ 常见误区 ============

// ❌ 误区1:watchEffect 中使用条件分支导致依赖收集不完整
watchEffect(() => {
  if (userId.value > 0) {
    // 只有 userId > 0 时才会访问 filters.status
    // 所以 userId <= 0 时,filters.status 的变化不会触发重新执行!
    fetch(`/api?status=${filters.status}`)
  }
})
// ✅ 解决:把所有需要追踪的依赖放在条件外面先"读"一下
watchEffect(() => {
  const id = userId.value
  const status = filters.status  // 确保被追踪
  if (id > 0) {
    fetch(`/api?status=${status}`)
  }
})

// ❌ 误区2:watch 监听 reactive 属性忘记用 getter
watch(filters.status, (val) => { /* ... */ })
// ⚠️ 这里 filters.status 是一个字符串值 'active',不是响应式的!
// ✅ 正确写法
watch(() => filters.status, (val) => { /* ... */ })

// ❌ 误区3:watchEffect 中使用异步操作后的响应式访问不会被追踪
watchEffect(async () => {
  const res = await fetch('/api')
  // ⚠️ await 之后的代码在微任务中执行
  // 此时 watchEffect 的同步追踪阶段已结束
  console.log(userId.value)  // 这个访问 **不会** 被追踪为依赖!
})
// ✅ 解决:在 await 之前先访问所有需要追踪的依赖
watchEffect(async () => {
  const id = userId.value     // ✅ 在 await 之前访问
  const res = await fetch(`/api/users/${id}`)
  data.value = await res.json()
})

选择指南(面试答题模板):

复制代码
用 watch 当你需要:
├── 获取旧值对比(newVal vs oldVal)
├── 精确监听某个特定数据源
├── 惰性执行(只在变化时才触发)
├── 配合 immediate / deep / once 等选项精细控制
└── 监听 props 的变化

用 watchEffect 当你需要:
├── 依赖多个响应式数据,不想手动列出
├── 默认立即执行一次(初始化逻辑)
├── 副作用与依赖自动绑定(类似 React useEffect 但更智能)
└── 代码更简洁、不关心旧值

7. Pinia 状态管理(全面详解)

Pinia 是 Vue3 官方推荐的状态管理库,替代 Vuex。

Pinia vs Vuex 对比:

特性 Pinia Vuex
Vue 版本 Vue2/Vue3 Vue2(Vuex3) / Vue3(Vuex4)
mutations 没有(直接修改 state) ✅ 必须通过 mutations
模块化 天然支持(每个 store 独立) modules + namespaced
TypeScript ✅ 完美支持 ❌ 需要额外类型声明
体积 ~1KB ~10KB
DevTools
SSR ✅ 简单 需要额外配置
组合式写法 setup() 风格
javascript 复制代码
// ========== 安装与配置 ==========
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())

// ========== 选项式 Store ==========
// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  getters: {
    double: (state) => state.count * 2,
    // getter 使用其他 getter
    doublePlusOne(): number {
      return this.double + 1  // ✅ 通过 this 访问其他 getter
    }
  },
  actions: {
    increment() { this.count++ },
    async fetchCount() {
      const { data } = await api.getCount()
      this.count = data.count
      // ✅ action 中可以直接修改 state(没有 mutations 的概念)
    }
  }
})

// ========== 组合式 Store(推荐) ==========
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // ref → state
  const userInfo = ref<UserInfo | null>(null)
  const token = ref(localStorage.getItem('token') || '')

  // computed → getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => userInfo.value?.name ?? '游客')

  // function → actions
  async function login(credentials: LoginParams) {
    const { data } = await api.login(credentials)
    token.value = data.token
    userInfo.value = data.userInfo
    localStorage.setItem('token', data.token)
  }

  async function logout() {
    await api.logout()
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }

  function $reset() {
    token.value = ''
    userInfo.value = null
  }

  return { userInfo, token, isLoggedIn, userName, login, logout, $reset }
})

// ========== 组件中使用 ==========
// <script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// ⚠️ 直接解构会丢失响应式
const { isLoggedIn } = userStore           // ❌ 不是响应式的
const { isLoggedIn } = storeToRefs(userStore)  // ✅ storeToRefs 保持响应式
// 注意:actions 不需要 storeToRefs,直接解构
const { login, logout } = userStore  // ✅ 方法直接解构

// 直接修改 state
userStore.token = 'new-token'        // ✅ 直接修改
userStore.$patch({ token: 'new' })   // ✅ 批量修改
userStore.$patch(state => {          // ✅ 函数式批量修改
  state.token = 'new'
  state.userInfo = null
})
userStore.$reset()                   // ✅ 重置为初始值
// </script>
javascript 复制代码
// ========== Store 间互相调用 ==========
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))

  async function checkout() {
    const userStore = useUserStore()  // ✅ 在 action 中调用其他 store
    if (!userStore.isLoggedIn) throw new Error('请先登录')
    await api.checkout(items.value)
    items.value = []
  }

  return { items, total, checkout }
})

// ========== Pinia 插件 ==========
// plugins/piniaPersistedState.ts(持久化插件)
import { watch } from 'vue'

export function piniaPersistedState({ store }) {
  // 从 localStorage 恢复
  const saved = localStorage.getItem(`pinia-${store.$id}`)
  if (saved) store.$patch(JSON.parse(saved))

  // 订阅变化自动保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
  })
}

// main.ts
const pinia = createPinia()
pinia.use(piniaPersistedState)

// ========== 推荐使用 pinia-plugin-persistedstate ==========
// npm install pinia-plugin-persistedstate
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)

// store 中启用
export const useUserStore = defineStore('user', {
  state: () => ({ token: '', userInfo: null }),
  persist: {
    key: 'user-store',
    storage: localStorage,      // 或 sessionStorage
    pick: ['token'],             // 只持久化 token
  }
})

💡 面试加分点: Pinia 去掉 mutations 的设计理由------mutations 在 Vuex 中的目的是让 DevTools 追踪状态变化,但 Pinia 通过 $subscribe 同样实现了追踪,不需要额外概念。组合式 Store 中 $reset() 需要手动实现(选项式自动支持)。


8. Vue Router 4(Vue3 版本)全面详解

javascript 复制代码
// ========== 创建路由 ==========
import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),  // History 模式
  // history: createWebHashHistory(),   // Hash 模式
  // history: createMemoryHistory(),    // 内存模式(SSR)
  routes: [
    {
      path: '/',
      component: () => import('@/layouts/DefaultLayout.vue'),
      children: [
        { path: '', name: 'Home', component: () => import('@/views/Home.vue') },
        { path: 'about', name: 'About', component: () => import('@/views/About.vue') },
      ]
    },
    // 动态路由
    { path: '/user/:id(\\d+)', name: 'User', component: () => import('@/views/User.vue'), props: true },
    // 可选参数
    { path: '/search/:keyword?', name: 'Search', component: () => import('@/views/Search.vue') },
    // 命名视图(一个页面多个 router-view)
    {
      path: '/admin',
      components: {
        default: () => import('@/views/Admin.vue'),
        sidebar: () => import('@/components/AdminSidebar.vue')
      }
    },
    // 重定向
    { path: '/home', redirect: '/' },
    { path: '/user-home', redirect: { name: 'User', params: { id: 1 } } },
    // 别名
    { path: '/dashboard', component: Dashboard, alias: ['/panel', '/cp'] },
    // 404 通配符(必须放最后)
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue') }
  ],
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    if (to.hash) return { el: to.hash, behavior: 'smooth' }
    return { top: 0, behavior: 'smooth' }
  }
})

路由模式对比:

特性 createWebHistory createWebHashHistory createMemoryHistory
URL /user/1 /#/user/1 无 URL 变化
服务器配置 ✅ 需要 fallback ❌ 不需要 -
SEO ✅ 友好 -
使用场景 生产环境 兼容场景 SSR / 测试

9. Vue Router 4 导航守卫(完整)

javascript 复制代码
// ========== 完整导航流程 ==========
// 1. 导航触发
// 2. 失活组件:onBeforeRouteLeave
// 3. 全局:beforeEach
// 4. 重用组件:onBeforeRouteUpdate
// 5. 路由配置:beforeEnter
// 6. 解析异步路由组件
// 7. 全局:beforeResolve
// 8. 导航确认
// 9. 全局:afterEach
// 10. DOM 更新

// ========== 全局前置守卫(权限控制) ==========
const whiteList = ['/login', '/register', '/404']

router.beforeEach(async (to, from) => {
  // Vue Router 4 不再需要 next(),返回值即可
  const userStore = useUserStore()

  // 设置页面标题
  document.title = (to.meta.title as string) || '默认标题'

  // 白名单直接放行
  if (whiteList.includes(to.path)) return true

  // 未登录 → 跳登录页
  if (!userStore.token) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  // 已登录但无用户信息 → 获取用户信息
  if (!userStore.userInfo) {
    try {
      await userStore.getUserInfo()
      // 动态路由:获取权限后添加路由
      const routes = await generateRoutes(userStore.roles)
      routes.forEach(route => router.addRoute(route))
      return to.fullPath  // 重新导航(确保动态路由已添加)
    } catch {
      userStore.logout()
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }

  // 权限检查
  if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
    return { name: 'Forbidden' }
  }

  return true  // 放行
})

// ========== 全局后置钩子 ==========
router.afterEach((to, from, failure) => {
  if (failure) console.error('导航失败:', failure)
  NProgress.done()  // 关闭进度条
})

// ========== 路由独享守卫 ==========
{
  path: '/admin',
  component: Admin,
  beforeEnter: (to) => {
    if (!isAdmin()) return { name: 'Forbidden' }
  }
}

// ========== 组件内守卫(Composition API) ==========
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 路由参数变化时(如 /user/1 → /user/2,组件被复用)
onBeforeRouteUpdate(async (to) => {
  const userData = await fetchUser(to.params.id)
  user.value = userData
})

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

💡 面试加分点: Vue Router 4 的守卫不再需要 next()------通过 return false(取消)、return { name: 'xxx' }(重定向)、return true/不返回(放行)控制。router.addRoute() 实现动态路由是后台管理系统权限控制的核心方案。


10. Vue3 组件通信方式(全面总结)

方式 方向 适用场景
props / emit 父 ↔ 子 基本父子通信
v-model 父 ↔ 子 双向绑定(支持多个)
defineModel 父 ↔ 子 v-model 简化写法(3.4+)
provide / inject 祖先 → 后代 跨层级(✅ 响应式)
$attrs 父 → 子 属性透传(含事件)
defineExpose / ref 父 → 子 访问子组件方法/属性
Pinia 任意 全局状态管理
mitt 任意 事件总线(替代 EventBus)
javascript 复制代码
// ========== 1. props + emit ==========
// 子组件
const props = defineProps<{ title: string; count?: number }>()
const emit = defineEmits<{ update: [value: string]; delete: [id: number] }>()
emit('update', 'new value')

// ========== 2. v-model(Vue3 支持多个) ==========
// 父组件:<MyForm v-model:name="name" v-model:age="age" />
// 子组件
const props = defineProps(['name', 'age'])
const emit = defineEmits(['update:name', 'update:age'])

// ========== 3. defineModel(Vue 3.4+,极简) ==========
// 子组件
const name = defineModel<string>('name')
const age = defineModel<number>('age', { default: 18 })
// 直接读写,自动同步到父组件
name.value = '新名字'

// ========== 4. provide / inject(Vue3 中是响应式的) ==========
// 祖先组件
const theme = ref('dark')
const toggleTheme = () => { theme.value = theme.value === 'dark' ? 'light' : 'dark' }
provide('theme', { theme: readonly(theme), toggleTheme })  // ✅ readonly 防止子组件修改

// 后代组件
const { theme, toggleTheme } = inject('theme', { theme: ref('light'), toggleTheme: () => {} })
// 推荐使用 InjectionKey 保证类型安全
import type { InjectionKey, Ref } from 'vue'
const ThemeKey: InjectionKey<{ theme: Readonly<Ref<string>>; toggle: () => void }> = Symbol('theme')

// ========== 5. $attrs(Vue3 合并了 $listeners) ==========
// Vue3 中 $attrs 包含所有未被 props/emits 声明的属性和事件
// <Child class="custom" @click="handler" data-id="1" />
const attrs = useAttrs()
// attrs = { class: 'custom', onClick: handler, 'data-id': '1' }

// ========== 6. defineExpose + ref ==========
// 子组件
const inputRef = ref<HTMLInputElement>()
const focus = () => inputRef.value?.focus()
defineExpose({ focus })

// 父组件
const childRef = ref<InstanceType<typeof ChildComponent>>()
onMounted(() => childRef.value?.focus())

// ========== 7. mitt 事件总线 ==========
// eventBus.ts
import mitt from 'mitt'
type Events = {
  'user-login': { name: string }
  'theme-change': string
}
export const emitter = mitt<Events>()

// 发送
emitter.emit('user-login', { name: '张三' })
// 监听
emitter.on('user-login', (data) => console.log(data.name))
// 清理(在 onUnmounted 中)
onUnmounted(() => emitter.off('user-login', handler))

💡 面试加分点: Vue3 的 $attrs 合并了 Vue2 的 $attrs$listeners,事件以 onXxx 形式存在。provide/inject 配合 readonly()InjectionKey 是推荐的跨层级通信方式------比 Pinia 更轻量,比 props 层层传递更优雅。


11. Vue3 的虚拟 DOM 和 Diff 算法优化

Vue3 编译优化三板斧:Patch Flag + Block Tree + 静态提升

javascript 复制代码
// 模板:
// <div>
//   <p>静态文本</p>
//   <p>{{ message }}</p>
//   <p :class="cls">动态class</p>
// </div>

// Vue3 编译后(伪代码):
const _hoisted = createVNode('p', null, '静态文本')  // ✅ 静态提升:只创建一次

function render() {
  return createBlock('div', null, [
    _hoisted,                                            // 直接复用
    createVNode('p', null, msg.value, 1 /* TEXT */),     // PatchFlag=1 只比较文本
    createVNode('p', { class: cls.value }, '动态', 2 /* CLASS */), // PatchFlag=2 只比较class
  ])
}

// ========== Patch Flag 枚举 ==========
// 1  = TEXT        动态文本
// 2  = CLASS       动态 class
// 4  = STYLE       动态 style
// 8  = PROPS       动态属性
// 16 = FULL_PROPS  带有动态 key 的属性
// 32 = HYDRATE_EVENTS  需要事件监听器
// 64 = STABLE_FRAGMENT 子节点顺序不变的 fragment
// -1 = HOISTED     静态提升
// -2 = BAIL        需要完整 diff

// ========== Block Tree ==========
// 将模板中的动态节点收集为扁平数组,diff 时只遍历动态节点
// 传统 diff:遍历整棵 VNode 树
// Block Tree:只遍历 dynamicChildren 数组

// ========== 最长递增子序列(LIS)优化节点移动 ==========
// 旧:[A, B, C, D, E]
// 新:[A, D, B, C, E]
// LIS 找到 [A, B, C, E](不需要移动),只移动 D

💡 面试加分点: Vue3 编译优化让运行时性能提升 2-6 倍。Patch Flag 在编译时就确定了需要 diff 的属性类型,运行时可以跳过不变的部分。Block Tree 将 O(模板大小) 降为 O(动态节点数)。


12. Teleport 和 Suspense

html 复制代码
<!-- ========== Teleport:将内容渲染到指定 DOM 位置 ========== -->
<template>
  <button @click="showModal = true">打开弹窗</button>

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

  <!-- 可以传送到任何 CSS 选择器匹配的元素 -->
  <Teleport to="#notifications">
    <Toast v-if="showToast" :message="toastMsg" />
  </Teleport>

  <!-- disabled:条件性禁用传送 -->
  <Teleport to="body" :disabled="isMobile">
    <ContextMenu />
  </Teleport>
</template>

<!-- ========== Suspense:处理异步组件加载状态 ========== -->
<template>
  <Suspense>
    <template #default>
      <AsyncDashboard />  <!-- 异步组件(setup 中有 await) -->
    </template>
    <template #fallback>
      <LoadingSkeleton />  <!-- 加载中的占位 -->
    </template>
  </Suspense>
</template>

<!-- 配合 onErrorCaptured 处理加载失败 -->
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)
onErrorCaptured((err) => {
  error.value = err
  return false
})
</script>
<template>
  <div v-if="error">加载失败:{{ error.message }}</div>
  <Suspense v-else>
    <template #default><AsyncComponent /></template>
    <template #fallback><div>加载中...</div></template>
  </Suspense>
</template>
javascript 复制代码
// 异步 setup 组件(可以在 setup 中使用 await)
// AsyncDashboard.vue
// <script setup>
const data = await fetch('/api/dashboard').then(r => r.json())
// Suspense 会等待这个异步 setup 完成
// </script>

💡 面试加分点: Teleport 解决了模态框/弹窗的 z-index 和样式隔离问题。Suspense 目前仍是实验性功能 ,但在 Nuxt 3 中已大量使用。多个 Teleport 可以传送到同一目标元素,按顺序追加。


13. 自定义指令(Vue3 版本)

javascript 复制代码
// Vue3 指令钩子与组件生命周期一致
const vDirective = {
  created(el, binding, vnode, prevVnode) {},     // 元素属性/事件应用前
  beforeMount(el, binding) {},                    // 挂载到 DOM 前
  mounted(el, binding) {},                        // 挂载后(最常用)
  beforeUpdate(el, binding) {},                   // VNode 更新前
  updated(el, binding) {},                        // VNode 更新后
  beforeUnmount(el, binding) {},                  // 卸载前
  unmounted(el, binding) {}                       // 卸载后
}

// ========== <script setup> 中自动注册:以 v 开头的变量 ==========
// <script setup>
// v-focus 指令
const vFocus = { mounted: (el) => el.focus() }

// v-permission 权限指令
const vPermission = {
  mounted(el, binding) {
    const userStore = useUserStore()
    if (!userStore.permissions.includes(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

// v-loading 加载指令
const vLoading = {
  mounted(el, binding) {
    el.style.position = 'relative'
    if (binding.value) addLoadingMask(el)
  },
  updated(el, binding) {
    if (binding.value !== binding.oldValue) {
      binding.value ? addLoadingMask(el) : removeLoadingMask(el)
    }
  }
}

// v-debounce 防抖指令
const vDebounce = {
  mounted(el, binding) {
    const { value: fn, arg = '300' } = binding
    let timer
    el.addEventListener('click', () => {
      clearTimeout(timer)
      timer = setTimeout(() => fn(), parseInt(arg))
    })
  }
}

// v-click-outside 点击外部
const vClickOutside = {
  mounted(el, binding) {
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value(e)
    }
    document.addEventListener('click', el._handler)
  },
  unmounted(el) {
    document.removeEventListener('click', el._handler)
  }
}
// </script>

// <template>
//   <input v-focus />
//   <button v-permission="'admin:delete'">删除</button>
//   <div v-loading="isLoading">内容</div>
//   <button v-debounce:500="handleSubmit">提交</button>
//   <div v-click-outside="closeMenu">菜单</div>
// </template>

💡 面试加分点: <script setup> 中以 v 开头的变量自动注册为指令。Vue3 的指令钩子名与组件生命周期一致(mounted/updated/unmounted),Vue2 用的是 bind/inserted/update/unbind


14. keep-alive 缓存组件

html 复制代码
<!-- Vue3 的 keep-alive 配合 router-view -->
<template>
  <router-view v-slot="{ Component, route }">
    <transition :name="route.meta.transition || 'fade'" mode="out-in">
      <keep-alive :include="cachedViews" :max="10">
        <component :is="Component" :key="route.fullPath" />
      </keep-alive>
    </transition>
  </router-view>
</template>

<script setup>
import { ref, onActivated, onDeactivated } from 'vue'

// 动态控制缓存列表
const cachedViews = ref(['Home', 'List', 'Dashboard'])
const addCache = (name: string) => {
  if (!cachedViews.value.includes(name)) cachedViews.value.push(name)
}
const removeCache = (name: string) => {
  cachedViews.value = cachedViews.value.filter(v => v !== name)
}
provide('cacheControl', { addCache, removeCache })
</script>
javascript 复制代码
// 被缓存组件内部
// <script setup>
import { onActivated, onDeactivated, ref } from 'vue'

const scrollTop = ref(0)

onActivated(() => {
  // 从缓存恢复时触发
  fetchLatestData()
  window.scrollTo(0, scrollTop.value)
})

onDeactivated(() => {
  // 进入缓存时触发
  scrollTop.value = document.documentElement.scrollTop
})
// </script>

💡 面试加分点: keep-alivemax 使用 LRU 算法------超出限制时销毁最久没被访问的组件。缓存组件不触发 onMounted/onUnmounted,而是 onActivated/onDeactivated。配合 <Transition> 可实现页面切换动画。


15. Vue3 的 v-model 机制

html 复制代码
<!-- Vue3 的 v-model = modelValue + update:modelValue -->
<!-- <CustomInput v-model="value" /> 等价于 -->
<CustomInput :modelValue="value" @update:modelValue="val => value = val" />

<!-- ✅ Vue3 支持多个 v-model -->
<UserForm v-model:name="userName" v-model:age="userAge" v-model:email="userEmail" />
javascript 复制代码
// ========== 传统写法 ==========
// CustomInput.vue
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
// <template><input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /></template>

// ========== defineModel 写法(Vue 3.4+,推荐) ==========
const model = defineModel<string>()
// <template><input v-model="model" /></template>

// 多个 v-model
const name = defineModel<string>('name')
const age = defineModel<number>('age', { default: 0 })

// ========== v-model 修饰符 ==========
// <MyInput v-model.capitalize="text" />
const [model, modifiers] = defineModel<string>({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})

💡 面试加分点: Vue2 的 v-model 一个组件只能用一个(用 .sync 做额外双向绑定),Vue3 废弃 .sync,用多个 v-model:xxx 替代。defineModel(3.4+)将 defineProps + defineEmits + 手动同步简化为一行代码。


16. 插槽(Slots)详解

html 复制代码
<!-- ========== 默认插槽 + 具名插槽 ========== -->
<!-- Card.vue -->
<template>
  <div class="card">
    <header v-if="$slots.header"><slot name="header" /></header>
    <main><slot>默认内容</slot></main>
    <footer v-if="$slots.footer"><slot name="footer" /></footer>
  </div>
</template>

<!-- 使用 -->
<Card>
  <template #header><h2>标题</h2></template>
  <p>主要内容</p>
  <template #footer><button>确定</button></template>
</Card>

<!-- ========== 作用域插槽 ========== -->
<!-- DataTable.vue -->
<template>
  <table>
    <tr v-for="row in data" :key="row.id">
      <td v-for="col in columns" :key="col.key">
        <slot :name="col.key" :row="row" :value="row[col.key]">
          {{ row[col.key] }}
        </slot>
      </td>
    </tr>
  </table>
</template>

<!-- 使用(自定义某列的渲染) -->
<DataTable :data="users" :columns="columns">
  <template #status="{ row, value }">
    <span :class="value === 'active' ? 'green' : 'red'">
      {{ value === 'active' ? '启用' : '禁用' }}
    </span>
  </template>
  <template #actions="{ row }">
    <button @click="edit(row)">编辑</button>
    <button @click="del(row.id)">删除</button>
  </template>
</DataTable>
javascript 复制代码
// <script setup> 中检测插槽
import { useSlots } from 'vue'
const slots = useSlots()
const hasHeader = computed(() => !!slots.header)

// TypeScript 类型安全的插槽(Vue 3.3+)
defineSlots<{
  default(props: { msg: string }): any
  header(props: { title: string }): any
  item(props: { data: Item; index: number }): any
}>()

💡 面试加分点: 作用域插槽是组件库开发的核心------让父组件决定"怎么渲染",子组件提供"渲染什么数据"。Vue3 的 defineSlots(3.3+)提供了编译时类型检查。


17. Vue3 性能优化全攻略

分类 优化手段 说明
编译层 Tree-shaking 未使用的 API 不打包
静态提升 静态节点只创建一次
Patch Flag 运行时只 diff 动态部分
组件层 路由懒加载 () => import('./xxx.vue')
异步组件 defineAsyncComponent()
v-show vs v-if 频繁切换用 v-show
v-once 只渲染一次
v-memo 缓存列表项(3.2+)
响应式 shallowRef/shallowReactive 减少深层响应式开销
markRaw 标记永远不需要响应式的对象
computed 缓存 替代复杂模板表达式
列表 唯一 key 不用 index
虚拟滚动 大列表(vue-virtual-scroller)
网络 keep-alive 缓存 避免重复请求
图片懒加载 v-lazy / IntersectionObserver
按需导入 第三方库按需引入
javascript 复制代码
// ========== v-memo(Vue 3.2+):条件性缓存列表项 ==========
// <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
//   只有 item.selected 变化时才重新渲染这个节点
//   {{ item.name }} - {{ item.description }} - {{ item.selected }}
// </div>

// ========== shallowRef + triggerRef 优化大数据 ==========
import { shallowRef, triggerRef, markRaw } from 'vue'

const bigList = shallowRef(new Array(100000).fill(null).map((_, i) => ({ id: i, name: `item${i}` })))
// 修改内部不触发更新(性能好)
bigList.value[0].name = 'updated'
triggerRef(bigList)  // 手动触发更新

// ========== markRaw:标记不需要响应式的对象 ==========
import { markRaw } from 'vue'
const chart = markRaw(new ECharts(dom))  // ECharts 实例不需要响应式
const state = reactive({
  chart: markRaw(new ECharts(dom)),  // 在 reactive 中也不会被代理
  map: markRaw(new Map())            // 超大 Map 不需要响应式
})

// ========== defineAsyncComponent 异步组件 ==========
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSkeleton,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 10000,
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) retry()
    else fail()
  }
})

💡 面试加分点: v-memo 是 Vue 3.2 的高级优化------对大列表只重新渲染"数据真正变化"的项。markRaw 适合第三方库实例(ECharts、地图 SDK 等),避免 Proxy 代理产生兼容问题和性能开销。


18. Vue3 错误处理机制

javascript 复制代码
// ========== 1. 全局错误处理器 ==========
const app = createApp(App)

app.config.errorHandler = (err, instance, info) => {
  console.error('全局错误:', err)
  console.error('错误组件:', instance?.$options?.name || instance)
  console.error('错误来源:', info)  // 如 "setup function" "render function" "watcher"
  // 上报到 Sentry / 自建监控
  Sentry.captureException(err, { extra: { info, component: instance?.$options?.name } })
}

app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue 警告:', msg)  // 仅开发环境
}

// ========== 2. 组件级错误边界 ==========
// ErrorBoundary.vue
// <script setup>
import { onErrorCaptured, ref, h } from 'vue'

const error = ref<Error | null>(null)
const retry = ref(0)

onErrorCaptured((err, instance, info) => {
  error.value = err as Error
  return false  // 阻止向上传播
})

const reset = () => { error.value = null; retry.value++ }
// </script>
// <template>
//   <div v-if="error" class="error-boundary">
//     <p>出错了: {{ error.message }}</p>
//     <button @click="reset">重试</button>
//   </div>
//   <slot v-else :key="retry" />
// </template>

// 使用
// <ErrorBoundary>
//   <RiskyComponent />
// </ErrorBoundary>

// ========== 3. 路由错误处理 ==========
router.onError((error) => {
  // 处理动态导入失败(部署更新后旧 chunk 404)
  if (error.message.includes('Failed to fetch dynamically imported module')) {
    window.location.reload()
  }
})

// ========== 4. API 请求统一错误处理 ==========
// composables/useRequest.ts
export function useRequest<T>(fn: () => Promise<T>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fn() as T
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, execute }
}

💡 面试加分点: Vue3 自动捕获 setup()、生命周期钩子、watch/watchEffect 中的异步错误。路由懒加载失败(chunk 404)是常见的线上问题,通过 router.onError 检测并自动刷新页面是标准解决方案。


19. Vue3 + TypeScript 最佳实践

typescript 复制代码
// ========== 组件 Props 类型 ==========
// <script setup lang="ts">
interface Props {
  title: string
  count?: number
  status: 'active' | 'inactive'
  user?: { name: string; age: number }
  list: Item[]
  onChange?: (value: string) => void
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  status: 'active',
  user: () => ({ name: '', age: 0 }),
  list: () => []
})

// ========== Emits 类型 ==========
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
  change: [value: string, oldValue: string]
}>()

// ========== ref 类型 ==========
import { ref, type Ref } from 'vue'
const count = ref<number>(0)
const user = ref<User | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

// ========== reactive 类型 ==========
interface State {
  loading: boolean
  data: User[]
  error: Error | null
}
const state: State = reactive({
  loading: false,
  data: [],
  error: null
})

// ========== computed 类型 ==========
const double = computed<number>(() => count.value * 2)

// ========== provide / inject 类型安全 ==========
import type { InjectionKey } from 'vue'

interface ThemeContext {
  theme: Readonly<Ref<'light' | 'dark'>>
  toggle: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

// 提供
provide(ThemeKey, { theme: readonly(theme), toggle: toggleTheme })
// 注入(有类型推断)
const themeCtx = inject(ThemeKey)!  // ! 断言非空

// ========== 全局组件类型声明 ==========
// env.d.ts 或 global.d.ts
declare module 'vue' {
  interface ComponentCustomProperties {
    $filters: { currency: (v: number) => string }
    $http: typeof axios
  }
}
// </script>

💡 面试加分点: Vue3 + TS 的核心类型工具:PropType<T>(选项式 props 类型)、InjectionKey<T>(provide/inject 类型)、InstanceType<typeof Comp>(组件实例类型)。withDefaults 解决了 defineProps<T>() 无法直接设置默认值的问题。


20. Vue3 新特性总结(3.0 - 3.5)

版本 重要特性
3.0 Composition API、Teleport、Suspense、Fragment、Proxy 响应式
3.1 onServerPrefetchdefineAsyncComponent 改进
3.2 <script setup> 正式、v-memov-bind in CSSdefineCustomElement
3.3 defineOptionsdefineSlotsdefineEmits 简化语法、泛型组件
3.4 defineModelv-bind 同名简写、watchonce 选项、Proxy 性能优化
3.5 响应式 Props 解构、useTemplateRef、SSR 改进、onWatcherCleanup
html 复制代码
<!-- ========== v-bind in CSS(3.2+) ========== -->
<script setup>
const color = ref('red')
const fontSize = ref(16)
</script>
<style scoped>
.text {
  color: v-bind(color);
  font-size: v-bind(fontSize + 'px');  /* 支持表达式 */
}
</style>

<!-- ========== 泛型组件(3.3+) ========== -->
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{ list: T[]; selected?: T }>()
defineEmits<{ select: [item: T] }>()
</script>

<!-- ========== v-bind 同名简写(3.4+) ========== -->
<img :id="id" :src="src" :alt="alt" />
<!-- 简写为 -->
<img :id :src :alt />

<!-- ========== 响应式 Props 解构(3.5+) ========== -->
<script setup>
const { title, count = 0 } = defineProps<{ title: string; count?: number }>()
// 直接解构且保持响应式!(3.5 之前解构会丢失响应式)
watchEffect(() => console.log(title, count))
</script>

<!-- ========== useTemplateRef(3.5+) ========== -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const inputEl = useTemplateRef('my-input')  // 通过字符串匹配 ref
onMounted(() => inputEl.value?.focus())
</script>
<template>
  <input ref="my-input" />
</template>

💡 面试加分点: 面试中能提到 Vue 3.3-3.5 的新特性说明你持续关注技术发展。defineModel(3.4)简化双向绑定、响应式 Props 解构(3.5)解决了长期痛点、泛型组件(3.3)让类型安全的组件库开发成为可能。


21. SSR(服务端渲染)和 Nuxt 3

渲染方式 说明 首屏速度 SEO 服务器压力
CSR 客户端渲染(SPA)
SSR 服务端渲染 ✅ 快
SSG 静态站点生成 ✅ 最快 ❌ 无
ISR 增量静态生成 ✅ 快
javascript 复制代码
// ========== Vue3 原生 SSR ==========
// server.js
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'

const app = createSSRApp({
  data: () => ({ count: 0 }),
  template: `<button @click="count++">{{ count }}</button>`
})

const html = await renderToString(app)
// → '<button>0</button>'

// ========== Nuxt 3 核心概念 ==========
// 自动路由(基于文件系统)
// pages/
// ├── index.vue         → /
// ├── about.vue         → /about
// ├── users/
// │   ├── index.vue     → /users
// │   └── [id].vue      → /users/:id

// 数据获取
// <script setup>
const { data, pending, error, refresh } = await useFetch('/api/users')
const { data: user } = await useAsyncData('user', () => $fetch(`/api/users/${route.params.id}`))
// </script>

// 自动导入(不需要 import)
// - Vue API(ref、computed、watch...)
// - 组合函数(useFetch、useState、useRoute...)
// - components/ 下的组件

// ========== Nuxt 3 常用功能 ==========
// 状态管理
const counter = useState('counter', () => 0)

// 中间件
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()
  if (!isLoggedIn.value && to.path !== '/login') {
    return navigateTo('/login')
  }
})

// SEO
useHead({ title: '页面标题', meta: [{ name: 'description', content: '描述' }] })
useSeoMeta({ title: '标题', ogTitle: '分享标题', description: '描述' })

💡 面试加分点: SSR 的核心挑战是"注水"(Hydration)------服务端返回 HTML,客户端接管时将事件绑定到已有 DOM 上。Nuxt 3 基于 Nitro 引擎,支持部署到 Node.js、Vercel、Cloudflare Workers 等多种平台。


22. Vue3 的 Transition 和动画

html 复制代码
<!-- ========== 基本过渡 ========== -->
<Transition name="fade" mode="out-in">
  <component :is="currentView" />
</Transition>

<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

<!-- ========== 列表过渡 ========== -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>

<style>
.list-enter-active, .list-leave-active { transition: all 0.5s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
.list-move { transition: transform 0.5s ease; }  /* FLIP 动画 */
.list-leave-active { position: absolute; }  /* 离开时脱离文档流 */
</style>

<!-- ========== JavaScript 钩子 ========== -->
<Transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @leave="onLeave"
  :css="false"
>
  <div v-if="show">动画内容</div>
</Transition>

<script setup>
function onEnter(el, done) {
  gsap.to(el, { opacity: 1, duration: 0.5, onComplete: done })
}
function onLeave(el, done) {
  gsap.to(el, { opacity: 0, duration: 0.5, onComplete: done })
}
</script>

<!-- ========== 路由过渡 ========== -->
<router-view v-slot="{ Component, route }">
  <Transition :name="route.meta.transition || 'fade'" mode="out-in">
    <component :is="Component" />
  </Transition>
</router-view>

💡 面试加分点: <TransitionGroup> 使用 FLIP 动画技术实现列表项的平滑移动。mode="out-in" 让旧元素先离开、新元素再进入,避免两个元素同时存在。结合 GSAP 等动画库可实现复杂动画。


23. Vue3 项目最佳实践

bash 复制代码
src/
├── api/              # API 请求封装
│   ├── modules/      # 按模块分
│   │   ├── user.ts
│   │   └── product.ts
│   └── request.ts    # axios 实例和拦截器
├── assets/           # 静态资源
├── components/       # 通用组件(全局复用)
│   ├── ui/           # 基础 UI 组件
│   └── business/     # 业务通用组件
├── composables/      # 组合函数(useXxx)
│   ├── useFetch.ts
│   ├── useAuth.ts
│   └── usePermission.ts
├── directives/       # 自定义指令
├── layouts/          # 布局组件
├── pages/ (views/)   # 页面组件
├── router/           # 路由配置
│   ├── index.ts
│   ├── guards.ts     # 导航守卫
│   └── routes/       # 路由模块
├── stores/           # Pinia 状态管理
│   ├── user.ts
│   └── app.ts
├── styles/           # 全局样式
├── types/            # TypeScript 类型
├── utils/            # 工具函数
├── App.vue
└── main.ts
javascript 复制代码
// ========== 1. Props 规范 ==========
// ✅ TypeScript 类型声明 + withDefaults
interface Props {
  title: string
  status: 'active' | 'inactive'
  list?: Item[]
}
const props = withDefaults(defineProps<Props>(), {
  list: () => []
})

// ========== 2. Composable 规范 ==========
// ✅ 命名以 use 开头,返回 ref(方便解构)
export function usePagination(fetchFn: (params: PageParams) => Promise<PageResult>) {
  const page = ref(1)
  const pageSize = ref(10)
  const total = ref(0)
  const list = ref<any[]>([])
  const loading = ref(false)

  const fetch = async () => {
    loading.value = true
    try {
      const { data, total: t } = await fetchFn({ page: page.value, pageSize: pageSize.value })
      list.value = data
      total.value = t
    } finally { loading.value = false }
  }

  watch([page, pageSize], fetch, { immediate: true })

  return { page, pageSize, total, list, loading, refetch: fetch }
}

// ========== 3. 状态管理选择 ==========
// 简单状态 → composable + provide/inject
// 全局共享 → Pinia store
// 服务端状态 → TanStack Query (vue-query)

// ========== 4. 编码规范 ==========
// ✅ 组件名:PascalCase 多单词(UserProfile, TodoList)
// ✅ composable:useFetch, useAuth, usePermission
// ✅ store:useUserStore, useCartStore
// ✅ Props:camelCase(驼峰)
// ✅ Emit:kebab-case(短横线)
// ✅ 模板中组件:PascalCase 或 kebab-case

💡 面试加分点: 项目架构能力是高级前端的核心------组件拆分原则(单一职责)、composable 提取时机(逻辑复用 ≥ 2 个组件)、状态管理选型(简单用 composable,复杂用 Pinia,服务端缓存用 vue-query)。


24. Vue3 与 Vite

特性 Vite Vue CLI (Webpack)
启动速度 ✅ 毫秒级(原生 ESM) ❌ 秒级(打包后启动)
HMR 速度 ✅ 极快(按需编译) 慢(全量构建)
构建工具 Rollup Webpack
配置复杂度 简单 复杂
生态 快速发展 成熟但停止维护
javascript 复制代码
// ========== vite.config.ts ==========
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    // 自动导入 Vue API 和组件
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    alias: { '@': '/src' }
  },
  server: {
    proxy: {
      '/api': { target: 'http://localhost:3000', changeOrigin: true }
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          elementPlus: ['element-plus']
        }
      }
    }
  }
})

// ========== 环境变量 ==========
// .env.development
// VITE_API_URL=http://localhost:3000
// .env.production
// VITE_API_URL=https://api.example.com

// 使用
const apiUrl = import.meta.env.VITE_API_URL

💡 面试加分点: Vite 的核心原理------开发时利用浏览器原生 ES Module,按需编译(不打包),所以启动极快。生产构建用 Rollup。unplugin-auto-importunplugin-vue-components 是 Vue3 项目的标配插件。


25. Vue3 的 provide/inject 详解

核心概念

provide/inject 是 Vue3 中跨层级组件通信的首选方案 ,祖先组件 provide 数据,后代组件 inject 注入,中间层无需逐级传递。

特性 Vue2 provide/inject Vue3 provide/inject
响应式 ❌ 默认不响应(除非传对象引用) ✅ 传 ref/reactive 即可响应
类型安全 ✅ 配合 InjectionKey 泛型
使用位置 Options API Composition API / Options API
适用场景 跨层级主题/配置 组件库设计、跨层级状态共享
typescript 复制代码
// ========== 1. 基本用法 ==========
// 祖先组件
import { provide, ref, readonly } from 'vue'

const theme = ref('dark')
const userInfo = reactive({ name: 'Tom', role: 'admin' })

// ✅ 提供响应式数据
provide('theme', theme)
// ✅ 提供只读数据(防止子组件直接修改)
provide('userInfo', readonly(userInfo))
// ✅ 提供修改方法(单向数据流)
provide('updateTheme', (val: string) => { theme.value = val })

// 后代组件(任意深度)
import { inject } from 'vue'

const theme = inject('theme')                    // Ref<string>
const userInfo = inject('userInfo')              // DeepReadonly<{...}>
const updateTheme = inject('updateTheme')        // (val: string) => void

// ========== 2. 默认值和必填校验 ==========
// 有默认值
const theme = inject('theme', 'light')           // 未提供时用 'light'
const list = inject('list', () => [], true)      // 工厂函数默认值(第三参数 true 表示是工厂)

// 无默认值(如果未 provide,值为 undefined)
const data = inject('data')  // string | undefined

类型安全的 InjectionKey

typescript 复制代码
// ========== 3. 使用 InjectionKey 实现类型安全(推荐) ==========
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'

export interface UserInfo {
  name: string
  role: 'admin' | 'editor' | 'viewer'
  permissions: string[]
}

// ✅ 定义带类型的 key(Symbol 避免命名冲突)
export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
export const userKey: InjectionKey<UserInfo> = Symbol('user')
export const updateThemeKey: InjectionKey<(val: string) => void> = Symbol('updateTheme')

// 祖先组件
import { themeKey, userKey, updateThemeKey } from '@/types/injection-keys'

provide(themeKey, theme)            // ✅ 类型自动推断,传错类型会报错
provide(userKey, userInfo)

// 后代组件
const theme = inject(themeKey)!     // Ref<string>(! 断言非空)
const user = inject(userKey)        // UserInfo | undefined

实际应用场景

typescript 复制代码
// ========== 4. 组件库设计:Form + FormItem ==========
// Form.vue
const formData = reactive({})
const rules = ref({})

provide('formContext', {
  model: formData,
  rules: rules,
  validate: () => { /* 校验逻辑 */ },
  resetFields: () => { /* 重置逻辑 */ }
})

// FormItem.vue(不管嵌套多深都能拿到)
const formContext = inject('formContext')!
// 使用 formContext.model、formContext.rules 等

// ========== 5. provide/inject vs Pinia 选型 ==========
// ✅ 用 provide/inject:
//   - 组件库内部通信(Form/Tabs/Accordion 等)
//   - 局部的主题/配置共享
//   - 不需要跨页面持久化的数据

// ✅ 用 Pinia:
//   - 全局状态(用户信息、购物车、权限)
//   - 需要 DevTools 调试
//   - 需要持久化(localStorage/sessionStorage)
//   - 需要在组件外(路由守卫、API 拦截器)访问

💡 面试加分点: provide/inject 的最佳实践:1)用 Symbol 作为 key (避免命名冲突);2)用 InjectionKey 泛型 (类型安全);3)provide readonly 数据 + 修改方法 (单向数据流,防止子组件越权修改);4)组件库设计中大量使用(如 Element Plus 的 Form/Table/Tabs 内部通信都用 provide/inject)。和 Pinia 的区别:provide/inject 是"组件树局部共享",Pinia 是"全局状态管理"。


26. Vue3 的 shallowRef / shallowReactive / toRaw / markRaw

为什么需要这些 API?

Vue3 默认对对象做深层响应式代理,但有些场景不需要深度监听,反而会带来性能开销。

API 作用 适用场景
shallowRef 只监听 .value 的赋值,不深层代理 大型对象/类实例/第三方库对象
shallowReactive 只代理对象第一层属性 只关心顶层属性变化
toRaw 获取响应式对象的原始对象 传给第三方库/深拷贝/性能敏感操作
markRaw 标记对象永远不会变成响应式 类实例/不可变大数据/第三方库实例
triggerRef 手动触发 shallowRef 的依赖更新 修改了 shallowRef 深层数据后手动通知
typescript 复制代码
// ========== 1. shallowRef:只监听 .value 赋值 ==========
import { shallowRef, triggerRef } from 'vue'

const bigList = shallowRef([
  { id: 1, name: 'item1', details: { /* 大量嵌套数据 */ } },
  { id: 2, name: 'item2', details: { /* 大量嵌套数据 */ } },
  // ... 几千条
])

// ❌ 不会触发更新(修改了深层属性,但 .value 引用没变)
bigList.value[0].name = 'new name'

// ✅ 方式1:替换整个 .value 才会触发
bigList.value = [...bigList.value]

// ✅ 方式2:修改后手动触发
bigList.value[0].name = 'new name'
triggerRef(bigList)  // 手动通知依赖更新

// ========== 2. shallowReactive:只代理第一层 ==========
import { shallowReactive } from 'vue'

const state = shallowReactive({
  count: 0,            // ✅ 响应式(第一层)
  nested: {
    deep: 'hello'      // ❌ 非响应式(深层)
  }
})

state.count++              // ✅ 触发更新
state.nested.deep = 'hi'  // ❌ 不触发更新
state.nested = { deep: 'hi' }  // ✅ 替换整个属性才触发

toRaw 和 markRaw

typescript 复制代码
// ========== 3. toRaw:获取原始对象(去掉 Proxy) ==========
import { reactive, toRaw } from 'vue'

const state = reactive({ name: 'Tom', age: 18 })

// 传给第三方库时(避免 Proxy 干扰)
const raw = toRaw(state)  // 返回原始对象,没有 Proxy 包裹
chart.binbindData(raw)     // ✅ ECharts/D3 等库接收纯对象

// 性能敏感的操作(跳过 Proxy 的 getter/setter 拦截)
const rawList = toRaw(bigList.value)
const result = rawList.filter(item => item.active)  // ✅ 更快,不会触发 track

// 深拷贝时(避免拷贝 Proxy)
const copy = JSON.parse(JSON.stringify(toRaw(state)))

// ========== 4. markRaw:永远不变成响应式 ==========
import { markRaw, reactive } from 'vue'

class MyChart {
  binbindData(data) { /* ... */ }
  bindbindEvents() { /* ... */ }
  bindDestroy() { /* ... */ }
}

// ✅ 标记为"永不响应式"
const chart = markRaw(new MyChart())

const state = reactive({
  chart,              // ✅ chart 不会被转为 Proxy(即使放在 reactive 里)
  data: [1, 2, 3]     // ✅ 正常响应式
})

// 常见需要 markRaw 的对象:
// - ECharts / D3 实例
// - 大型不可变数据(地图 GeoJSON、字典表等)
// - 类实例(new SomeClass())
// - window / document 等浏览器对象

性能优化实战

typescript 复制代码
// ========== 实际场景:大列表 + ECharts ==========
import { shallowRef, markRaw } from 'vue'
import * as echarts from 'echarts'

// ✅ 大列表用 shallowRef(不需要监听每条数据的深层变化)
const tableData = shallowRef<TableRow[]>([])

// ✅ ECharts 实例用 markRaw(不需要响应式)
const chartInstance = shallowRef<echarts.ECharts | null>(null)

onMounted(() => {
  chartInstance.value = markRaw(echarts.init(chartRef.value!))
})

// 更新数据时替换引用
const fetchData = async () => {
  const data = await api.getList()
  tableData.value = data       // ✅ 整体替换,触发更新
  chartInstance.value?.setOption({ series: [{ data }] })
}

onUnmounted(() => {
  chartInstance.value?.dispose()
})

💡 面试加分点: 这组 API 的核心思想是"按需响应式 "------不是所有数据都需要深度监听。大列表用 shallowRef(只关心整体替换),第三方库实例用 markRaw(不需要响应式),传给外部库时用 toRaw(去掉 Proxy 壳)。这是 Vue3 性能优化的重要手段,面试中提到说明你对响应式系统理解深入。


27. Vue3 的异步组件(defineAsyncComponent)

基本用法

typescript 复制代码
// ========== 1. 基本异步加载 ==========
import { defineAsyncComponent } from 'vue'

// 最简写法
const AsyncComp = defineAsyncComponent(() => import('./HeavyComponent.vue'))

// 完整配置
const AsyncCompWithOptions = defineAsyncComponent({
  // 加载函数
  loader: () => import('./HeavyComponent.vue'),
  // 加载中显示的组件
  loadingComponent: LoadingSpinner,
  // 加载失败显示的组件
  errorComponent: ErrorDisplay,
  // 显示 loading 前的延迟(避免闪烁),默认 200ms
  delay: 200,
  // 超时时间(超时后显示 errorComponent)
  timeout: 10000,
  // 是否可挂起(配合 Suspense)
  suspensible: false,
  // 错误时的回调
  onError(error, retry, fail, attempts) {
    if (error.message.includes('fetch') && attempts <= 3) {
      retry()  // 网络错误自动重试(最多 3 次)
    } else {
      fail()   // 其他错误直接失败
    }
  }
})

// ========== 2. 在路由中使用(路由懒加载) ==========
const routes = [
  {
    path: '/dashboard',
    // 这本质上就是异步组件
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/admin',
    // 带 webpackChunkName / vite 的 chunk 命名
    component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
  }
]

配合 Suspense 使用

html 复制代码
<!-- ========== 3. 配合 Suspense(推荐) ========== -->
<template>
  <Suspense>
    <!-- 异步组件(或内部有 async setup 的组件) -->
    <template #default>
      <AsyncDashboard />
    </template>
    <!-- 加载中的 fallback -->
    <template #fallback>
      <div class="loading">
        <Spinner />
        <p>加载中...</p>
      </div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent(
  () => import('./Dashboard.vue')
)
</script>

<!-- ========== 4. async setup 的组件(天然异步组件) ========== -->
<!-- Dashboard.vue -->
<script setup>
// 组件内有顶层 await → 自动变成异步组件
const data = await fetch('/api/dashboard').then(r => r.json())
const config = await fetch('/api/config').then(r => r.json())
</script>

<template>
  <div>{{ data.title }}</div>
</template>

与 Vue2 异步组件的对比

javascript 复制代码
// ========== Vue2 异步组件写法 ==========
// 简写
Vue.component('async-comp', () => import('./MyComp.vue'))

// 高级配置
Vue.component('async-comp', () => ({
  component: import('./MyComp.vue'),
  loading: LoadingComp,
  error: ErrorComp,
  delay: 200,
  timeout: 3000
}))

// ========== Vue3 的改进 ==========
// 1. 用 defineAsyncComponent 包裹(更明确)
// 2. 支持 onError 回调(可以重试)
// 3. 支持 suspensible 选项(配合 Suspense)
// 4. loader 选项(名称更语义化)

💡 面试加分点: 异步组件的本质是代码分割 (Code Splitting),让组件的 JS 代码在需要时才加载,而不是打包到主 bundle 中。路由懒加载 () => import('./View.vue') 就是最常见的异步组件用法。Vue3 的 defineAsyncComponent 比 Vue2 更强大------支持加载重试(onError + retry)、超时控制、与 Suspense 配合。<script setup> 中使用顶层 await 会自动变成异步组件,需要父组件用 <Suspense> 包裹。


28. Vue3.4+ 的 defineModel

为什么需要 defineModel?

Vue3 中实现 v-model 需要手动定义 props + emit,代码比较繁琐。defineModel(Vue 3.4+)极大简化了这个流程。

html 复制代码
<!-- ========== 之前的写法(Vue 3.0 - 3.3):繁琐 ========== -->
<script setup>
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

// 需要手动同步
const updateValue = (val: string) => {
  emit('update:modelValue', val)
}
</script>

<template>
  <input :value="props.modelValue" @input="updateValue($event.target.value)" />
</template>

<!-- 父组件使用 -->
<!-- <MyInput v-model="username" /> -->
html 复制代码
<!-- ========== defineModel 写法(Vue 3.4+):极简 ========== -->
<script setup>
// ✅ 一行代码搞定!返回一个 ref,可直接读写
const modelValue = defineModel<string>()
// 等价于自动帮你:
// 1. 声明 prop: modelValue
// 2. 声明 emit: update:modelValue
// 3. 返回一个可读写的 ref(写入时自动 emit)
</script>

<template>
  <!-- ✅ 直接用 v-model 绑定这个 ref -->
  <input v-model="modelValue" />
</template>

<!-- 父组件使用(完全一样) -->
<!-- <MyInput v-model="username" /> -->

进阶用法

html 复制代码
<!-- ========== 1. 带选项的 defineModel ========== -->
<script setup>
// 必填 + 默认值
const title = defineModel<string>('title', {
  required: true,
  default: '未命名'
})

// 带校验
const count = defineModel<number>('count', {
  default: 0,
  validator: (value) => value >= 0
})
</script>

<!-- 父组件 -->
<!-- <MyComp v-model:title="pageTitle" v-model:count="num" /> -->

<!-- ========== 2. 多个 v-model ========== -->
<script setup>
// 默认 model
const modelValue = defineModel<string>()           // v-model
// 具名 model
const firstName = defineModel<string>('firstName')  // v-model:firstName
const lastName = defineModel<string>('lastName')     // v-model:lastName
</script>

<template>
  <input v-model="firstName" placeholder="名" />
  <input v-model="lastName" placeholder="姓" />
  <p>全名:{{ firstName }} {{ lastName }}</p>
</template>

<!-- 父组件 -->
<!-- <NameInput v-model:firstName="first" v-model:lastName="last" /> -->

<!-- ========== 3. 带修饰符的 defineModel ========== -->
<script setup>
// v-model.capitalize="text"
const [modelValue, modifiers] = defineModel<string>({
  // 定义 set 转换器:写入前自动处理
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input v-model="modelValue" />
</template>

<!-- 父组件 -->
<!-- <MyInput v-model.capitalize="text" /> -->

实际应用:封装表单组件

html 复制代码
<!-- ========== 封装一个通用的搜索输入框 ========== -->
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watchDebounced } from '@vueuse/core'

const keyword = defineModel<string>({ default: '' })
const emit = defineEmits<{ search: [keyword: string] }>()

// 防抖搜索
watchDebounced(keyword, (val) => {
  emit('search', val)
}, { debounce: 300 })
</script>

<template>
  <div class="search-input">
    <input
      v-model="keyword"
      placeholder="请输入搜索关键词"
      @keyup.enter="emit('search', keyword)"
    />
    <button @click="keyword = ''">清空</button>
  </div>
</template>

<!-- 父组件使用 -->
<!-- <SearchInput v-model="searchText" @search="handleSearch" /> -->

💡 面试加分点: defineModel 是 Vue 3.4 最重要的 DX 改进------将 v-model 的 props + emit 模板代码从 5-6 行压缩到 1 行。它返回的是一个 ref,读取时等于读 prop,写入时自动触发 emit 。支持多 v-model(defineModel('name'))、修饰符(modifiers)、校验器(validator)。在封装表单组件库时,defineModel 能让代码量减少 60% 以上。


29. 高频综合面试题

Q: Vue3 中如何实现权限控制?

javascript 复制代码
// ========== 1. 路由级权限(动态路由) ==========
// 登录后根据角色动态添加路由
const asyncRoutes = [
  { path: '/admin', component: Admin, meta: { roles: ['admin'] } },
  { path: '/editor', component: Editor, meta: { roles: ['admin', 'editor'] } }
]

function generateRoutes(roles: string[]) {
  return asyncRoutes.filter(route =>
    !route.meta?.roles || route.meta.roles.some(role => roles.includes(role))
  )
}

// 在路由守卫中动态添加
router.beforeEach(async (to) => {
  const userStore = useUserStore()
  if (userStore.token && !userStore.routes.length) {
    const routes = generateRoutes(userStore.roles)
    routes.forEach(route => router.addRoute(route))
    userStore.setRoutes(routes)
    return to.fullPath  // 重新导航
  }
})

// ========== 2. 按钮级权限(指令 + composable) ==========
// composables/usePermission.ts
export function usePermission() {
  const userStore = useUserStore()
  const hasPermission = (permission: string | string[]) => {
    const perms = Array.isArray(permission) ? permission : [permission]
    return perms.some(p => userStore.permissions.includes(p))
  }
  return { hasPermission }
}

// 权限指令
const vPermission = {
  mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
    const { hasPermission } = usePermission()
    if (!hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

// 使用
// <button v-permission="'user:delete'">删除</button>
// const { hasPermission } = usePermission()
// <button v-if="hasPermission('user:delete')">删除</button>

Q: 如何封装 Axios 请求?

typescript 复制代码
// api/request.ts
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/user'
import router from '@/router'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 15000
})

// 请求拦截器
service.interceptors.request.use((config) => {
  const userStore = useUserStore()
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`
  }
  return config
})

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, data, message } = response.data
    if (code === 0) return data
    // 业务错误
    ElMessage.error(message || '请求失败')
    return Promise.reject(new Error(message))
  },
  (error) => {
    if (error.response?.status === 401) {
      const userStore = useUserStore()
      userStore.logout()
      router.push({ name: 'Login', query: { redirect: router.currentRoute.value.fullPath } })
    } else if (error.response?.status === 403) {
      router.push({ name: 'Forbidden' })
    } else {
      ElMessage.error(error.message || '网络错误')
    }
    return Promise.reject(error)
  }
)

// 封装请求方法
export function get<T>(url: string, params?: object): Promise<T> {
  return service.get(url, { params })
}
export function post<T>(url: string, data?: object): Promise<T> {
  return service.post(url, data)
}

// api/modules/user.ts
export const userApi = {
  login: (data: LoginParams) => post<LoginResult>('/auth/login', data),
  getUserInfo: () => get<UserInfo>('/user/info'),
  getUsers: (params: PageParams) => get<PageResult<User>>('/users', params)
}

💡 面试加分点: 权限控制分三层:路由级(动态路由 addRoute)、页面级(路由守卫 meta.roles)、按钮级(v-permission 指令 / usePermission composable)。Axios 封装要处理 token 注入、401 自动登出、业务错误码统一处理。

相关推荐
天才熊猫君2 小时前
从“改自己”到“改布局”——拖拽调整的通用设计思路
前端·javascript
何时梦醒2 小时前
CSS 三栏布局与 BFC 深度解析:Float、Flex、Grid 三种方案一网打尽
前端·css·面试
橘子星2 小时前
别只会用 Pick/Omit/Partial!从拼多多一道面试题搞懂 TS 全部内置工具类型
前端
嘟嘟07172 小时前
TypeScript 工具类型通关:Pick、Omit、Partial、Record 区别与 Omit 等价实现一次讲清
设计模式·面试·typescript
PC2005_cloud2 小时前
DSH 沙箱机制拆解:受限令牌为何会掐断 HTTPS 请求
前端·程序员
众人皆醒我独醉2 小时前
MLOps 流水线:从训练到推理的 CI/CD
面试·llm·gpu
喜欢睡觉2 小时前
TypeScript 工具类型:像拼积木一样玩转类型
前端
mmsx2 小时前
一个黑边 Bug 修了两版:自己算矩阵直接黑屏,借库重建只用了一行 setZoom
android·前端
丑过三八线2 小时前
001 -【FastAPI 入门教程】 FastAPI Helloword
前端·chrome·fastapi