Vue3 基础必须掌握要点及大厂面试重点

Cosolar 3 阅读 前端

本文系统梳理 Vue3 从入门到进大厂必须掌握的核心知识点,结合字节、阿里、腾讯、美团等一线互联网公司近年面试真题,从 API 使用到原理实现层层深入,适合有一定前端基础、准备 Vue3 面试或系统进阶的开发者阅读。

一、Vue3 核心变化概览

Vue3 相对于 Vue2 在性能API 设计TypeScript 支持逻辑复用四个维度做了根本性重构,这是面试官最爱问的"Vue3 带来了什么"的第一层答案。

1.1 六大核心新特性

维度 Vue2 Vue3
响应式原理 Object.defineProperty Proxy
API 风格 Options API Composition API + Options API 共存
逻辑复用 Mixins(命名冲突、来源不清) Composables(组合式函数)
根节点 单根节点(必须包裹 div) Fragments(多根节点)
全局 API Vue.use() 挂载到原型 createApp() 应用实例隔离
TypeScript 支持较弱,需装饰器 原生 TS 支持,类型推导完整

除此之外,Vue3 还新增了 Teleport(传送门)、Suspense(异步组件加载)、defineCustomElement(自定义元素)等内置能力;模板编译器重写,静态节点提升、补丁标记(PatchFlags)、块级树(Block Tree)等编译优化让运行时性能大幅提升。

1.2 性能提升数据(面试可量化引用)

  • 打包体积:Tree-shaking 友好,核心运行时从 ~20KB gzip 降至 ~10KB gzip。
  • 首次渲染:比 Vue2 快约 55%。
  • 更新渲染:比 Vue2 快约 133%。
  • 内存占用:比 Vue2 减少约 54%。

二、创建 Vue3 应用

2.1 createApp 与 mount

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)  // 创建应用实例(区别于 new Vue())
app.mount('#app')            // 挂载到 DOM

面试考点:为什么用 createApp 替代 new Vue()

  • Vue2 中 new Vue() 创建的是全局构造函数实例,全局 API(Vue.useVue.mixinVue.component)会污染所有实例,多个 Vue 应用共存时配置互相影响。
  • Vue3 中 createApp 返回独立的应用实例(App Context),每个 app 的 component、directive、mixin、config 互不干扰,可在同一页面创建多个互不影响的 Vue 应用。

2.2 全局 API 迁移对照

Vue2 Vue3
Vue.config.productionTip 移除
Vue.use(plugin) app.use(plugin)
Vue.mixin(mixin) app.mixin(mixin)
Vue.component(name, comp) app.component(name, comp)
Vue.directive(name, dir) app.directive(name, dir)
Vue.prototype.$http = ... app.config.globalProperties.$http = ...

三、Composition API 深度解析

Composition API 是 Vue3 最核心的变革,也是面试必考内容。它让相关逻辑可以组织在一起(按功能聚合),而非散落在 datamethodscomputedwatch 等选项中。

3.1 setup 函数

setup 是 Composition API 的入口,在组件创建之前执行(在 beforeCreate 之前,此时组件实例尚未创建,thisundefined)。

export default {
  props: { title: String },
  setup(props, context) {
    // props:响应式的父组件传值
    // context:非响应式对象,包含 attrs、slots、emit、expose
    console.log(props.title)
    return { /* 返回给模板使用 */ }
  }
}

<script setup> 语法糖(推荐写法)

<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

<script setup> 本质是编译期语法糖,优势:

  • 更少的样板代码(不用 return)
  • 更好的运行时性能(模板编译到同一作用域,无需代理)
  • 更好的 TypeScript 类型推导
  • 顶层变量、导入的组件/函数可直接在模板中使用

3.2 ref:基本类型响应式

ref 用于创建一个响应式的、可更改的 ref 对象,值通过 .value 访问。

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

底层原理

  • ref 对基本类型走的是 getter/setter 拦截(类的 class getter/setter),而非 Proxy。
  • 对对象类型,ref 内部会自动调用 reactive 进行深层转换。
  • 在模板中使用 ref 会被自动解包,不需要写 .value(顶层 ref)。

注意点

  • 当 ref 作为 reactive 对象的属性被访问或修改时,会自动解包:
    const count = ref(0)
    const state = reactive({ count })
    state.count // 0,不需要 .value
    state.count = 1 // 直接赋值
    
  • 但当 ref 是数组或 Map 这类集合类型的元素时,不会自动解包

3.3 reactive:对象类型响应式

reactive 返回对象的响应式代理(Proxy)。

import { reactive } from 'vue'

const state = reactive({
  name: 'Vue3',
  info: { version: '3.4' }
})
state.name = 'Vue3.4'

reactive 的局限性(面试高频):

  1. 不能用于基本类型(string、number、boolean 等),只对对象类型有效。
  2. 不能整体替换:直接将响应式对象赋值给新变量会丢失响应性(断开引用)。
  3. 解构丢失响应性:解构 reactive 对象的属性后,变量是普通值,不是响应式的。解决方法:
    • 使用 toRefstoRef 将属性转成 ref:
      const state = reactive({ name: 'A', age: 18 })
      const { name, age } = toRefs(state) // name、age 保持响应式
      
    • 推荐:优先用 ref 声明基础状态,避免 reactive 的解构陷阱。

3.4 ref 与 reactive 的选择建议(面试标准答案)

场景 推荐
基本类型(number、string、boolean) ref
单个对象、整体替换的状态 ref(对象 ref 内部自动 reactive)
表单数据、分组的对象字段 reactive
逻辑组合中需要解构/传递 ref(解构安全)
不确定类型或需要统一 .value 风格 ref

社区最佳实践:优先使用 ref,reactive 用于内聚的表单/分组状态。

3.5 computed:计算属性

import { ref, computed } from 'vue'

const firstName = ref('San')
const lastName = ref('Zhang')

// 只读计算属性
const fullName = computed(() => firstName.value + ' ' + lastName.value)

// 可写计算属性
const fullNameWritable = computed({
  get: () => firstName.value + ' ' + lastName.value,
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})

面试考点:computed vs methods 区别?

  • computed 有缓存:依赖不变时不会重新计算;methods 每次渲染都会执行。
  • computed 是响应式依赖追踪的,methods 不是。
  • computed 默认只读,需要可写时显式实现 get/set。

3.6 watch 与 watchEffect:侦听器

watch:显式指定侦听数据源,回调在数据变化时执行。

import { ref, reactive, watch } from 'vue'

const count = ref(0)
const state = reactive({ name: 'Vue' })

// 侦听 ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`)
})

// 侦听 getter 函数(reactive 属性需用函数返回)
watch(() => state.name, (newVal) => {
  console.log('name changed:', newVal)
})

// 侦听多个源
watch([count, () => state.name], ([count, name], [oldCount, oldName]) => {
  // ...
})

// 深度侦听(reactive 对象默认深度,但 ref 对象需要 deep: true)
watch(() => state, (newVal) => { /* ... */ }, { deep: true })

// 立即执行
watch(count, (newVal) => { /* ... */ }, { immediate: true })

watchEffect:立即执行传入的函数,自动追踪其内部依赖,依赖变化时重新执行。

import { ref, watchEffect } from 'vue'

const count = ref(0)
const stop = watchEffect(() => {
  console.log('count is:', count.value) // 自动追踪 count
})

count.value++ // 触发回调
stop()        // 停止侦听

watch vs watchEffect 对比(面试常问):

特性 watch watchEffect
依赖来源 显式指定 自动收集
首次执行 默认不执行(immediate: true 时执行) 立即执行一次
是否能拿到新旧值
适用场景 需要知道旧值、精确控制侦听源 依赖不确定、只关心副作用

3.7 toRef、toRefs、toRaw、markRaw

import { reactive, toRef, toRefs, toRaw, markRaw } from 'vue'

const state = reactive({ name: 'Vue', age: 3 })

// toRef:为 reactive 对象的某个属性创建 ref,保持响应式连接
const nameRef = toRef(state, 'name')
nameRef.value = 'Vue3' // 同步修改 state.name

// toRefs:将 reactive 对象所有属性转为 ref,常用于解构/展开
const refs = toRefs(state)
refs.name.value // 'Vue3'

// toRaw:获取 reactive 代理的原始对象(慎用,可能破坏响应式)
const raw = toRaw(state)

// markRaw:标记对象永远不转为响应式,用于第三方类实例、大型对象
const bigObj = markRaw({ /* ... */ })

3.8 shallowRef、shallowReactive、triggerRef

import { shallowRef, shallowReactive, triggerRef } from 'vue'

// shallowRef:只对 .value 的访问是响应式的,深层不追踪
const shallow = shallowRef({ a: { b: 1 } })
shallow.value.a.b = 2  // 不触发更新
shallow.value = { a: { b: 3 } }  // 触发更新(替换 .value)
triggerRef(shallow)    // 手动触发 shallowRef 的更新

// shallowReactive:只追踪根级属性,嵌套对象不是响应式
const state = shallowReactive({ a: { b: 1 } })
state.a = { b: 2 }     // 触发更新
state.a.b = 3          // 不触发更新

适用场景:大型数据结构(如图表配置、富文本编辑器数据)只需根级响应,可大幅减少性能开销。

3.9 readonly 与 shallowReadonly

import { reactive, readonly, shallowReadonly } from 'vue'

const state = reactive({ count: 0 })
const copy = readonly(state) // 只读代理,修改 copy 会警告
// copy.count++  // 警告且不生效
state.count++   // 原对象修改会同步到 copy

const shallow = shallowReadonly(state) // 仅根级只读,嵌套可改

四、响应式系统原理(面试核心深水区)

4.1 Vue2 的 Object.defineProperty 痛点

Vue2 使用 Object.defineProperty 递归遍历对象属性,通过 getter/setter 拦截读写。但存在以下问题:

  1. 无法监听新增/删除属性:需要 Vue.set / Vue.delete
  2. 无法监听数组索引和 length:需要重写数组方法(push、pop、splice 等 7 个方法)。
  3. 初始化时递归遍历:对象嵌套深时性能差,且对 Map、Set、WeakMap 等新数据结构不支持。

4.2 Vue3 的 Proxy 方案

Proxy 可以代理整个对象,支持:

  • 监听属性新增/删除(setdeleteProperty
  • 监听数组索引和 length
  • 支持 Map、Set、WeakMap、WeakSet
  • 惰性递归(只有访问到嵌套对象时才代理,性能更好)
// 极简响应式原理示意(面试手写常考简化版)
const targetMap = new WeakMap() // target -> depsMap
let activeEffect = null

function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key) // 收集依赖
      const res = Reflect.get(obj, key, receiver)
      // 惰性:访问到对象才递归代理
      if (res !== null && typeof res === 'object') return reactive(res)
      return res
    },
    set(obj, key, value, receiver) {
      const ok = Reflect.set(obj, key, value, receiver)
      trigger(obj, key) // 触发更新
      return ok
    },
    deleteProperty(obj, key) {
      const ok = Reflect.deleteProperty(obj, key)
      trigger(obj, key)
      return ok
    }
  })
}

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

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

function effect(fn) {
  activeEffect = fn
  fn() // 执行一次,触发 getter 收集依赖
  activeEffect = null
}

4.3 ref 的实现原理

ref 没有用 Proxy,而是用类的 getter/setter

class RefImpl {
  constructor(value) {
    this._value = convert(value) // 对象走 reactive
    this.dep = new Set()
  }
  get value() {
    trackEffects(this.dep)
    return this._value
  }
  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = convert(newVal)
      triggerEffects(this.dep)
    }
  }
}
function convert(v) { return isObject(v) ? reactive(v) : v }

4.4 依赖收集与触发流程(3 张图级描述)

组件渲染/effect 执行
        ↓
activeEffect = 当前副作用函数
        ↓
访问响应式属性 → 触发 getter → track()
        ↓
targetMap[target][key].add(effect)  // WeakMap → Map → Set
        ↓
修改属性 → 触发 setter → trigger()
        ↓
取出对应 key 的 effect Set,依次执行 → 组件重新渲染

4.5 编译优化:PatchFlags 与 Block Tree

Vue3 编译器在模板编译时会做静态分析,生成优化后的渲染函数:

  • 静态节点提升(HoistStatic):将不变化的节点/属性提升到渲染函数外,只创建一次。
  • 补丁标记(PatchFlags):给动态节点打标记,diff 时只对比标记的属性(文本、class、style、props 等),而非全量对比。
  • 块级树(Block Tree):将模板划分为 Block(根节点、v-if/v-for 分支等),每个 Block 内部用数组收集动态节点,diff 时直接遍历动态节点数组,跳过静态内容。
  • 缓存事件处理函数(cacheHandlers):内联事件函数缓存,避免每次渲染重新创建。
// <div>hello <span>{{ name }}</span></div> 编译后示意
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "hello ", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
    //                                                        ↑ PatchFlag=1 表示只有文本是动态的
  ]))
}

五、生命周期钩子

5.1 Vue2 与 Vue3 生命周期对照

Vue2 Options API Vue3 Options API Composition API(setup 内) 触发时机
beforeCreate beforeCreate 不需要(setup 本身就早于它) 实例初始化后,数据观测前
created created 不需要(setup 中直接写即可) 实例创建完成,数据观测已完成
beforeMount beforeMount onBeforeMount 挂载前,render 首次调用前
mounted mounted onMounted 挂载完成,DOM 已渲染
beforeUpdate beforeUpdate onBeforeUpdate 数据更新,虚拟 DOM 重渲染前
updated updated onUpdated 数据更新,DOM 已重新渲染
beforeDestroy beforeUnmount onBeforeUnmount 组件卸载前
destroyed unmounted onUnmounted 组件卸载完成
errorCaptured errorCaptured onErrorCaptured 捕获子组件错误时
onRenderTracked(开发模式) 渲染过程追踪到响应式依赖时
onRenderTriggered(开发模式) 响应式依赖触发组件重新渲染时
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
  window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

面试考点:Composition API 中为什么没有 beforeCreate/created?
答:setup 本身就在 beforeCreate 之前执行,此时组件实例还没创建,data/computed/methods 都还没初始化,所以 setup 中的同步代码就相当于 beforeCreate/created 的时机;异步逻辑可放在 onMounted 中。

六、组件通信全方案

6.1 父子通信方式汇总

方式 方向 适用场景
props / defineProps 父 → 子 父组件向子组件传递数据
emit / defineEmits 子 → 父 子组件向父组件发送事件
v-model 双向 表单类组件、封装输入组件
ref / expose 父 → 子(方法/属性) 父组件调用子组件方法
provide / inject 祖先 → 后代(跨层级) 深层嵌套传值、主题/配置注入
事件总线(mitt) 任意组件 非父子、简单场景(不推荐滥用)
Pinia / Vuex 任意组件 全局状态管理,复杂应用推荐
$attrs 父 → 子(透传) 二次封装组件,透传属性/事件
$slots / 作用域插槽 父 ↔ 子(内容分发) 内容分发、列表组件自定义渲染

6.2 defineProps 与 defineEmits(<script setup>

<!-- Child.vue -->
<script setup>
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})
const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', props.count + 1)
}
</script>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
<template>
  <Child :title="'Hello'" :count="count" @update="val => count = val" />
</template>

6.3 v-model 在 Vue3 中的变化

Vue3 中 v-model 默认 prop 名为 modelValue,事件名为 update:modelValue;支持多个 v-model 和自定义修饰符。

<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue', 'title'])
const emit = defineEmits(['update:modelValue', 'update:title'])
</script>
<template>
  <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
  <input :value="title" @input="emit('update:title', $event.target.value)" />
</template>

<!-- 父组件 -->
<Child v-model="name" v-model:title="docTitle" />

6.4 provide / inject 与响应式

// 祖先组件
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
provide('theme', readonly(theme)) // 提供只读版本防止子组件随意修改
provide('setTheme', (val) => { theme.value = val }) // 提供修改方法

// 后代组件
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数为默认值
const setTheme = inject('setTheme')

注意:provide/inject 默认不是响应式的,除非传入的是 ref/reactive 对象;建议用 readonly 包裹,避免子组件直接修改祖先状态。

6.5 defineExpose(子组件暴露给父组件)

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
defineExpose({ count, reset }) // 只有 expose 的内容父组件才能通过 ref 访问
</script>
<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref()
onMounted(() => {
  console.log(childRef.value.count) // 0
  childRef.value.reset()
})
</script>
<template>
  <Child ref="childRef" />
</template>

七、Vue Router 4 核心

Vue Router 4 是适配 Vue3 的路由版本,API 风格与 Vue3 保持一致(函数式创建)。

7.1 创建路由

// router/index.js
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: () => import('../views/About.vue') }, // 路由懒加载
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/User.vue'),
    props: true, // 将路由参数作为 props 传入组件
    children: [ /* 嵌套路由 */ ]
  },
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/404.vue') }
]

const router = createRouter({
  history: createWebHistory(), // HTML5 History 模式
  // history: createWebHashHistory(), // Hash 模式
  routes,
  scrollBehavior(to, from, savedPosition) {
    return savedPosition || { top: 0 }
  }
})

// 全局前置守卫
router.beforeEach((to, from) => {
  // 返回 false 取消导航,返回路径字符串或对象重定向
  const isAuthenticated = localStorage.getItem('token')
  if (to.meta.requiresAuth && !isAuthenticated) return '/login'
})

export default router

main.js 中注册

app.use(router)

7.2 组件中使用路由(Composition API)

import { useRouter, useRoute } from 'vue-router'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

const router = useRouter()  // 等同于 this.$router
const route = useRoute()    // 等同于 this.$route(响应式)

// 编程式导航
router.push('/about')
router.push({ name: 'User', params: { id: 1 } })
router.replace('/home')
router.go(-1)

// 路由守卫(组件内)
onBeforeRouteLeave((to, from) => { /* 离开当前组件前 */ })
onBeforeRouteUpdate((to, from) => { /* 当前路由改变但组件复用时 */ })

7.3 路由模式区别

模式 URL 形式 需要服务器配置 SEO
Hash /#/about 不需要 差(# 后的内容不会发给服务器)
History /about 需要(所有路径返回 index.html)
Memory(抽象) 无 URL 变化 不需要 用于 SSR/测试

7.4 路由懒加载与分包

// 按组分包(webpackChunkName)
const User = () => import(/* webpackChunkName: "user" */ '../views/User.vue')

Vite 中使用动态 import 天然支持分包。

八、Pinia 状态管理(Vue3 官方推荐)

Pinia 已在 Vue3 中正式取代 Vuex 成为官方推荐状态管理库。

8.1 为什么选 Pinia 而不是 Vuex

  • 没有 mutations,简化概念(state、getters、actions 三件套)。
  • 完整的 TypeScript 类型推导,无需额外类型声明。
  • 支持 Composition API 风格定义 Store。
  • 天然支持多个 Store,无需嵌套模块。
  • 轻量(约 1KB),内置 devtools 支持、时间旅行。
  • 支持插件扩展(持久化、路由同步等)。

8.2 定义与使用 Store

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

// 组合式 API 风格(推荐)
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() { count.value++ }
  function asyncIncrement() {
    setTimeout(() => { count.value++ }, 1000)
  }
  return { count, doubleCount, increment, asyncIncrement }
})

// Options API 风格也支持
// export const useCounterStore = defineStore('counter', {
//   state: () => ({ count: 0 }),
//   getters: { doubleCount: (s) => s.count * 2 },
//   actions: { increment() { this.count++ } }
// })
// main.js
import { createPinia } from 'pinia'
app.use(createPinia())
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
// 解构保持响应式需用 storeToRefs
const { count, doubleCount } = storeToRefs(counter)
// 方法可直接解构
const { increment, asyncIncrement } = counter
</script>
<template>
  <div>{{ count }} * 2 = {{ doubleCount }}</div>
  <button @click="increment">+1</button>
</template>

8.3 修改状态的几种方式

const counter = useCounterStore()

// 1. 直接修改(推荐)
counter.count++

// 2. $patch 批量修改(性能更好,只触发一次响应)
counter.$patch({ count: counter.count + 1, name: 'new' })
counter.$patch((state) => { state.count++; state.name = 'new' })

// 3. 通过 actions 修改(支持异步)
counter.increment()
counter.asyncIncrement()

// 4. $reset 重置到初始状态
counter.$reset()

8.4 Store 之间互相调用

// stores/user.js
import { defineStore } from 'pinia'
import { useCounterStore } from './counter'

export const useUserStore = defineStore('user', () => {
  const counter = useCounterStore() // 可在 action/getter 中使用其他 store
  const name = ref('')
  function setName(n) {
    name.value = n
    counter.increment() // 调用其他 store 的 action
  }
  return { name, setName }
})

8.5 持久化插件(pinia-plugin-persistedstate)

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// 在 defineStore 中配置
export const useUserStore = defineStore('user', () => { /* ... */ }, {
  persist: true // 全部持久化到 localStorage
  // persist: {
  //   paths: ['name', 'token'], // 指定字段
  //   storage: sessionStorage
  // }
})

九、Vue3 内置新组件

9.1 Fragments(片段)

Vue3 组件支持多根节点,无需额外包裹元素:

<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

注意:多根节点时 $attrs 需要显式绑定到某个元素(不会自动透传)。

9.2 Teleport(传送门)

Teleport 可以将组件的一部分 DOM 渲染到 DOM 树的其他位置(如 body 下),常用于 Modal、Tooltip、Toast、Notification 等需要脱离父级 overflow/z-index 上下文的场景。

<template>
  <button @click="show = true">打开弹窗</button>
  <Teleport to="body">
    <div v-if="show" class="modal">
      <h2>弹窗内容</h2>
      <button @click="show = false">关闭</button>
    </div>
  </Teleport>
</template>

to 属性支持 CSS 选择器字符串或 DOM 元素;支持多个 Teleport 共享目标,内容按顺序追加;支持 :disabled 属性动态禁用传送。

9.3 Suspense(异步组件加载边界)

Suspense 用于在等待异步组件/异步 setup 时显示 fallback 内容,类似 React Suspense。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))
</script>

异步组件还支持更细粒度配置:

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Heavy.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComp,
  delay: 200,         // 显示 loading 前的延迟 ms
  timeout: 10000,     // 超时时间
  suspensible: false  // 是否由父级 Suspense 控制
})

十、自定义指令

Vue3 自定义指令钩子变化:

// 注册全局指令
app.directive('focus', {
  mounted(el) { el.focus() }
})

// 局部指令(<script setup> 中以 vNameOfDirective 命名自动识别)
const vFocus = {
  mounted: (el) => el.focus()
}

10.1 指令钩子函数

钩子 触发时机
created 元素属性/事件绑定前(在 beforeMount 之前)
beforeMount 指令第一次绑定到元素,挂载前
mounted 元素挂载到 DOM 后
beforeUpdate 父组件更新前
updated 父组件及子组件更新后
beforeUnmount 元素卸载前
unmounted 元素卸载后

10.2 钩子参数

app.directive('demo', {
  mounted(el, binding, vnode, prevVnode) {
    // el:指令绑定的元素
    // binding.value:传递给指令的值(v-demo="value")
    // binding.arg:传给指令的参数(v-demo:arg)
    // binding.modifiers:修饰符对象(v-demo.modifier)
    // binding.instance:使用指令的组件实例
  }
})

十一、插槽(Slots)

Vue3 中插槽使用方式基本一致,但在 Composition API 中访问方式变化。

11.1 默认插槽、具名插槽、作用域插槽

<!-- 子组件 Layout.vue -->
<template>
  <div class="layout">
    <header><slot name="header" :user="user">默认头部</slot></header>
    <main><slot>默认内容</slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>
<script setup>
const user = { name: 'Zhang' }
</script>
<!-- 父组件 -->
<Layout>
  <template #header="{ user }">
    <h1>Welcome {{ user.name }}</h1>
  </template>
  <p>主内容</p>
  <template #footer>
    <p>底部</p>
  </template>
</Layout>

11.2 在 <script setup> 中访问 slots/attrs

import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs() // 包含父组件传入但未声明为 props 的属性

十二、性能优化

12.1 组件层面优化

  1. v-for 必须加 key:key 应为唯一 id,避免使用 index(列表增删时导致错误复用、状态错位)。
  2. v-if 与 v-for 不要同时用在同一元素:Vue3 中 v-if 优先级高于 v-for(与 Vue2 相反),可能导致意外行为;用 <template> 包裹或先用 computed 过滤。
  3. 合理使用 v-once / v-memo
    • v-once:只渲染一次,后续更新跳过。
    • v-memo="[]":指定依赖数组,依赖不变则跳过子树更新(类似 React useMemo)。
  4. 大列表虚拟滚动:使用 vue-virtual-scrollervueuc 等虚拟列表方案。
  5. 组件懒加载:路由/组件使用 defineAsyncComponent 按需加载。
  6. shallowRef / shallowReactive:大型不可变数据用浅层响应。
  7. markRaw:第三方类实例(如 ECharts、Three.js 对象)标记为非响应式。
  8. 冻结数据Object.freeze(data) 或直接使用非响应式普通对象展示只读数据。

12.2 构建层面优化

  1. Tree-shaking:按需引入(如 Element Plus 的 unplugin-auto-import / unplugin-vue-components)。
  2. 代码分包:路由级、组件级动态 import。
  3. Gzip/Brotli 压缩:服务器开启。
  4. CDN 外链:Vue、VueRouter、Pinia 等可用 CDN,配合 externals 减小打包体积。
  5. 图片优化:WebP、懒加载、雪碧图、响应式图片。
  6. SSR / SSG:Nuxt3、ViteSSG 提升首屏和 SEO。

12.3 运行时优化

  1. 长列表防抖节流:滚动、输入等高频事件。
  2. computed 替代复杂模板表达式:缓存计算结果。
  3. watch 精准侦听:避免 deep: true 滥用,必要时手动指定侦听路径。
  4. 避免在模板中调用函数(除非事件处理):函数每次渲染都会执行,用 computed 替代。

十三、TypeScript 与 Vue3 结合

Vue3 原生支持 TS,<script setup lang="ts"> 是推荐写法。

13.1 为 props 标注类型

<script setup lang="ts">
// 运行时声明 + TS 类型推导
interface Props {
  title: string
  count?: number
  list: number[]
}
const props = withDefaults(defineProps<Props>(), {
  count: 0
})

const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete'): void
}>()
</script>

13.2 ref / reactive 的类型标注

import { ref, reactive } from 'vue'

const count = ref<number>(0)
const list = ref<string[]>([])

interface User { name: string; age: number }
const user = reactive<User>({ name: 'Zhang', age: 18 })
// 或直接字面量,自动推导
const user2 = reactive({ name: 'Zhang', age: 18 })

13.3 组件实例类型(Template Ref)

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref<InstanceType<typeof Child> | null>(null)
onMounted(() => {
  childRef.value?.reset() // 可获得 Child 暴露方法的类型
})
</script>
<template>
  <Child ref="childRef" />
</template>

十四、Vue3 生态与新特性补充

14.1 组合式函数(Composables)

Composable 是 Vue3 中逻辑复用的核心模式,以 use 开头命名,内部使用 Composition API,返回响应式状态和方法。

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  function update(e) { x.value = e.pageX; y.value = e.pageY }
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  return { x, y }
}

// 组件中使用
const { x, y } = useMouse()

Composable 设计原则

  • use 开头命名。
  • 参数用 ref 传以保持响应性,内部用 unref 兼容普通值。
  • 返回普通对象,包含 ref 和方法。
  • 内部自行管理 onMounted/onUnmounted 等副作用。
  • 不依赖组件上下文,可在多个组件甚至非组件场景复用。

14.2 自定义 Hooks vs Mixins 对比

特性 Mixins(Vue2) Composables(Vue3)
命名冲突 易冲突,覆盖关系隐式 变量通过解构/重命名,无冲突
来源清晰 属性来源不清晰(模板里不知来自哪个 mixin) 显式 import,来源一目了然
逻辑组织 按选项类型分散(data/methods/computed) 按功能聚合在一起
类型支持 完美 TS 推导
复用间通信 难,需要全局或组件状态 参数/返回值天然支持

14.3 VueUse

@vueuse/core 是 Vue3 生态中最常用的工具库,提供 200+ 个高质量 Composables:

import { useLocalStorage, useDebounceFn, useIntersectionObserver, useDark } from '@vueuse/core'

const count = useLocalStorage('count', 0) // 自动持久化
const onScroll = useDebounceFn(() => { /* ... */ }, 200) // 防抖
const target = ref(null)
useIntersectionObserver(target, ([{ isIntersecting }]) => { /* 可见性监听 */ })
const isDark = useDark() // 暗黑模式

十五、大厂面试高频考点与真题解析

以下按主题分类整理近年字节、阿里、腾讯、美团、百度、快手等大厂 Vue3 面试高频题,并给出参考回答思路。

15.1 Vue3 vs Vue2 差异类

Q1:Vue3 相比 Vue2 有哪些改进?为什么要重写?

答题框架:

  1. 响应式系统升级:Proxy 替代 defineProperty,解决数组、新增/删除属性问题,支持 Map/Set,惰性代理性能更好。
  2. Composition API:解决 Options API 中逻辑分散、Mixins 命名冲突/来源不清、TS 支持差的问题,提升大型项目可维护性和逻辑复用能力。
  3. 编译优化:PatchFlags、Block Tree、静态提升、事件缓存,运行时 diff 更精准。
  4. 体积更小:Tree-shaking 友好,核心运行时 ~10KB gzip。
  5. 性能更高:首次渲染快 55%,更新快 133%,内存少 54%。
  6. TS 原生支持:源码重写为 TS,类型推导完整。
  7. 新内置能力:Teleport、Suspense、Fragments、自定义元素。
  8. 架构调整:Monorepo(packages 拆分为 reactivity、runtime-core、runtime-dom、compiler-core、compiler-dom 等独立包),可独立使用(如只用 @vue/reactivity 做响应式库)。

Q2:Vue3 为什么用 Proxy 代替 defineProperty?Proxy 有什么优势?有什么缺点?

优势:

  • 可直接监听整个对象而非属性,不需要递归遍历。
  • 可监听数组变化(索引、length)。
  • 可监听属性新增/删除。
  • 支持 Map、Set、WeakMap、WeakSet。
  • 有 13 种拦截方法(apply、construct、ownKeys 等),能力更强。

缺点:

  • Proxy 是 ES6 特性,不支持 IE(Vue3 不支持 IE11,如需兼容需用 Vue2.7 + @vue/composition-api)。
  • Proxy 无法 polyfill(defineProperty 可通过降级模拟)。
  • Proxy 代理后每次访问都会产生包装开销,但 Vue3 通过惰性代理+缓存优化,整体性能仍优于 Vue2。

15.2 Composition API 类

Q3:ref 和 reactive 的区别?为什么需要 ref?

  • ref 可用于任何类型(基本类型+对象),reactive 只能用于对象类型。
  • ref 通过 .value 访问,reactive 直接访问属性。
  • ref 对象在 reactive/模板中会自动解包。
  • reactive 解构后丢失响应式,ref 解构/传递安全(因为 ref 是对象引用)。
  • ref 底层对基本类型用 class getter/setter,对对象自动走 reactive。
  • 需要 ref 的原因:JS 基本类型是值传递,无法通过引用拦截修改,必须用对象包装(.value)才能追踪。

Q4:为什么推荐优先使用 ref 而不是 reactive?

  • ref 更灵活(支持所有类型)。
  • ref 解构安全,不会丢失响应性。
  • ref 整体替换更直观(xxx.value = newVal)。
  • ref 在 Composable 返回时更一致(都是 .value 风格)。
  • reactive 的 Proxy 在某些场景下行为不如 ref 直观(如解构、展开、传参)。

Q5:watch 和 watchEffect 的区别?什么时候用哪个?

参考第三章对比表。补充:

  • watch 适合需要精确控制侦听源、需要 oldValue 的场景(如搜索联想、路由参数变化)。
  • watchEffect 适合依赖不固定、只关心副作用执行的场景(如日志、自动保存、DOM 操作依赖多个 ref)。

Q6:setup 中为什么没有 this?setup 执行时机?

  • setup 在 beforeCreate 之前执行,此时组件实例尚未创建,data/methods/computed 都没初始化,所以 this 为 undefined(设计上也是 Composition API 要摆脱 this 绑定)。
  • Composition API 通过闭包和响应式系统管理状态,不需要 this。
  • setup 是同步的,顶层 await 需要用 <Suspense> 包裹。

15.3 响应式原理类

Q7:Vue3 响应式原理是什么?依赖收集是怎么做的?

参见第四章。核心要点:

  • Proxy 拦截 get/set/deleteProperty。
  • 全局 activeEffect 标记当前执行的副作用函数(组件渲染函数 / watch 回调 / computed)。
  • WeakMap(target) → Map(key) → Set(effects) 三级结构存储依赖。
  • get 时 track 收集,set 时 trigger 执行所有相关 effect。
  • 用 WeakMap 避免内存泄漏(target 被 GC 时自动清理)。
  • 用 Set 去重(同一 effect 多次访问同一属性不重复收集)。

Q8:Vue3 是怎么解决 Vue2 中数组监听问题的?

Vue2 重写了数组的 7 个变异方法(push/pop/shift/unshift/splice/sort/reverse),但无法监听通过索引直接修改(arr[0] = x)和修改 length。
Vue3 的 Proxy 可以拦截数组的所有操作:索引设置会触发 set trap,length 修改会触发 set trap,删除数组元素会触发 deleteProperty trap,因此不再需要重写数组方法

Q9:Vue3 的响应式是深度代理吗?性能如何优化?

Vue3 默认是深度响应式,但采用惰性代理:只有访问到嵌套对象时才递归创建 Proxy(相比 Vue2 初始化时一次性递归,大数据场景性能更好)。
对于已知浅层结构的数据,可用 shallowRef/shallowReactive 手动控制层级;对不需要响应的第三方实例用 markRaw

Q10:computed 原理是什么?为什么有缓存?

computed 内部维护 _dirty 标志:

  • 首次访问或依赖变化时,_dirty = true,执行 getter 计算值并缓存到 _value,然后 _dirty = false
  • 再次访问时若 _dirty = false,直接返回 _value,不重新计算。
  • 依赖变化时(trigger),会触发 computed 的 scheduler 将 _dirty 置为 true,并通知依赖它的 effect。
  • 所以 computed 是懒计算的,只有被读取且依赖变化时才重新计算。

15.4 组件与通信类

Q11:Vue3 组件通信有哪些方式?

参见第六章 6.1 表格。通常根据关系选择:父子用 props/emit,跨层级用 provide/inject,全局用 Pinia,组件方法调用用 ref+expose。

Q12:v-model 在 Vue3 中有什么变化?

  • 默认 prop 名从 value 改为 modelValue,事件从 input 改为 update:modelValue
  • 支持多个 v-model:v-model:title="docTitle"
  • 支持自定义修饰符:通过 modelModifiers prop 接收。
  • .sync 修饰符被移除,统一用 v-model:xxx 替代。

Q13:Vue3 中为什么要移除 .sync?

.sync 在 Vue2 中本质就是 v-model 的语法糖变体,造成两种双向绑定写法。Vue3 统一用 v-model + 参数的形式表达,API 更一致、学习成本更低。

Q14:nextTick 原理?Vue3 中怎么用?

import { nextTick } from 'vue'
async function update() {
  count.value++
  await nextTick() // DOM 更新完成后执行
  console.log(el.textContent)
}

原理:Vue 的更新是异步批量的,数据修改不会立即更新 DOM,而是放入队列,同一事件循环内多次修改合并。nextTick 将回调放入微任务队列(优先 Promise.then → MutationObserver → setImmediate → setTimeout 降级),在当前同步代码执行完、DOM 更新后执行。

15.5 生命周期与 Hooks 类

Q15:Composition API 中如何复用逻辑?和 Mixins 比有什么优势?

通过 Composables(组合式函数)。优势参见 14.2 对比表。

Q16:onMounted 能在 setup 顶层之外调用吗?为什么?

onMounted 必须在 setup 同步执行期间调用(或在 Composable 中 setup 期间调用)。原因是 Vue3 内部维护一个当前组件实例的全局变量currentInstance),在 setup 执行期间设置;setup 执行完后清除。如果在异步回调(如 setTimeout)中调用 onMounted,会因找不到当前实例而报警。这也是为什么 Composable 必须在 setup 顶层调用的原因。

15.6 虚拟 DOM 与 Diff 类

Q17:Vue3 的 Diff 算法相比 Vue2 有什么改进?

  • Vue2 采用双端对比算法,对首尾指针进行比较。
  • Vue3 借鉴了 iviinferno 的算法,采用:
    1. 头尾比对 + 最长递增子序列:先从头部和尾部比对相同节点,处理完新增/删除后,对中间乱序部分通过最长递增子序列算法求最小移动次数(比 Vue2 的递归双端对比更快)。
    2. PatchFlags 编译提示:diff 时只对比有 PatchFlag 标记的动态内容,跳过静态节点。
    3. Block Tree:将模板划分为块,块内用数组收集动态节点,diff 时直接遍历动态节点数组,不用递归全树。

Q18:为什么 v-for 必须用 key?用 index 作为 key 会有什么问题?

  • key 是 vnode 的唯一标识,用于 diff 时判断节点是否可复用。
  • 不用 key 时 Vue 采用就地复用策略(模式匹配),性能最好但状态可能错位。
  • 用 index 作为 key 时,列表头部插入/删除元素会导致后续元素 key 全部变化,引发不必要的 DOM 更新和组件状态错位(如 input 值错位、动画错乱)。
  • 正确做法:使用数据的唯一 id(如数据库主键)作为 key。

15.7 路由类

Q19:路由守卫有哪些?完整流程是什么?

全局守卫:beforeEachbeforeResolveafterEach
路由独享:beforeEnter
组件内:beforeRouteEnter(Vue3 用 onBeforeRouteLeave/onBeforeRouteUpdate,没有 beforeRouteEnter 因为 setup 在 mount 之前执行)

完整导航解析流程(面试必须背诵):

  1. 导航触发
  2. 失活组件调用 beforeRouteLeave
  3. 调用全局 beforeEach
  4. 重用组件调用 beforeRouteUpdate
  5. 路由配置 beforeEnter
  6. 解析异步路由组件
  7. 激活组件调用 beforeRouteEnter(Vue3 Composition 中无对应)
  8. 调用全局 beforeResolve
  9. 导航确认
  10. 调用全局 afterEach
  11. DOM 更新
  12. beforeRouteEnter 的 next 回调执行(Vue3 中用 onMounted 替代)

Q20:Hash 路由和 History 路由区别?History 部署刷新 404 怎么办?

  • Hash 模式基于 window.location.hashhashchange 事件,# 后内容不发送给服务器,无需服务器配置,兼容性好但 URL 丑、SEO 差。
  • History 模式基于 HTML5 history.pushState / replaceStatepopstate 事件,URL 美观、SEO 好,但需要服务器配置:将所有路由都回退到 index.html,由前端路由接管。Nginx 配置:
    location / {
      try_files $uri $uri/ /index.html;
    }
    

15.8 状态管理类

Q21:Pinia 和 Vuex 的区别?为什么 Vue 官方推荐 Pinia?

参见第八章 8.1。

Q22:Pinia 中直接修改 state 和用 action 修改有什么区别?什么时候必须用 action?

  • 直接修改(store.count++)和 $patch 都可以修改 state,不需要 mutation。
  • action 用于封装有业务逻辑的修改(如异步请求、多步操作、带校验的修改),保持状态修改的可追踪性和可维护性。
  • 没有强制要求必须用 action,但建议:简单直接修改可直接改,复杂/异步/可复用的逻辑放在 action 中。

15.9 性能优化类

Q23:Vue3 项目做过哪些性能优化?

从三个层面回答(参见第十二章):

  1. 编码层面:key 正确、v-if/v-for 不同用、v-once/v-memo、虚拟滚动、懒加载、shallowRef/markRaw、computed 缓存、防抖节流。
  2. 构建层面:路由分包、Tree-shaking、按需引入、Gzip、CDN、图片优化。
  3. 架构层面:SSR/SSG(Nuxt)、HTTP 缓存、Service Worker、预渲染。

Q24:Vue3 的 Tree-shaking 是怎么实现的?为什么 Vue2 做不到?

  • Tree-shaking 依赖 ES Module 的静态结构(import/export 静态可分析)。
  • Vue2 的全局 API 都挂载在 Vue 构造函数上(Vue.nextTickVue.setVue.component),是副作用导入,打包工具无法判断是否用到,无法摇掉。
  • Vue3 将所有 API 改为命名导出import { ref, reactive, nextTick } from 'vue'),未引用的 API 会被打包工具识别并移除。
  • 内置指令/组件(v-model、Transition、KeepAlive)也做了按需标记,仅在模板使用时才被打包。

15.10 SSR / Nuxt 类

Q25:Vue SSR 原理是什么?有什么坑?

  • SSR(服务端渲染):在服务器执行 Vue 组件渲染为 HTML 字符串,发送到浏览器,再"激活"(hydration)为可交互 SPA。
  • 优点:首屏快、SEO 好。
  • 缺点:服务器压力大、开发受限(beforeMount/mounted 不执行、不能用 window/document、第三方库需特殊处理)、hydration mismatch 问题。
  • Nuxt3 是基于 Vue3 + Vite 的 SSR 框架,约定式路由、自动导入、服务端接口(useFetch/useAsyncData)等大大简化 SSR 开发。

十六、大厂手写题精选

16.1 手写响应式(reactive + effect,简化版)

参见第四章 4.2 代码,面试时通常要求写出核心 Proxy 拦截和依赖收集触发流程。

16.2 手写 ref

function ref(value) {
  return {
    _value: typeof value === 'object' && value !== null ? reactive(value) : value,
    get value() {
      track(this, 'value')
      return this._value
    },
    set value(newVal) {
      if (newVal !== this._value) {
        this._value = typeof newVal === 'object' && newVal !== null ? reactive(newVal) : newVal
        trigger(this, 'value')
      }
    }
  }
}

16.3 手写深拷贝(考虑 Vue 响应式对象)

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  if (hash.has(obj)) return hash.get(obj)

  // 处理 Vue3 响应式对象:获取原始对象再克隆
  const raw = obj['__v_raw'] || obj  // Vue3 reactive 对象有 __v_raw 标识
  const clone = Array.isArray(raw) ? [] : {}
  hash.set(obj, clone)
  for (const key in raw) {
    if (raw.hasOwnProperty(key)) {
      clone[key] = deepClone(raw[key], hash)
    }
  }
  return clone
}

16.4 手写事件总线(mitt 简化版)

class EventBus {
  constructor() { this.events = Object.create(null) }
  on(event, fn) {
    (this.events[event] || (this.events[event] = [])).push(fn)
  }
  off(event, fn) {
    if (!this.events[event]) return
    if (!fn) { this.events[event] = null; return }
    this.events[event] = this.events[event].filter(f => f !== fn)
  }
  emit(event, ...args) {
    (this.events[event] || []).slice().forEach(fn => fn(...args))
  }
  once(event, fn) {
    const wrapper = (...args) => { fn(...args); this.off(event, wrapper) }
    this.on(event, wrapper)
  }
}

16.5 手写 Promise / 异步并发控制

(超出 Vue 范畴但常考,建议掌握 Promise.all/race/any/allSettled、async/await 错误处理、并发控制 p-limit 实现。)

16.6 手写虚拟 DOM 到真实 DOM(h 函数 + render 简化版)

function h(tag, props = {}, children = []) {
  return { tag, props, children }
}

function render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    container.appendChild(document.createTextNode(vnode))
    return
  }
  const el = document.createElement(vnode.tag)
  for (const [key, val] of Object.entries(vnode.props || {})) {
    if (key.startsWith('on')) {
      el.addEventListener(key.slice(2).toLowerCase(), val)
    } else {
      el.setAttribute(key, val)
    }
  }
  const children = Array.isArray(vnode.children) ? vnode.children : [vnode.children]
  children.forEach(child => render(child, el))
  container.appendChild(el)
}

16.7 手写防抖 / 节流

function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

function throttle(fn, interval) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last >= interval) {
      last = now
      fn.apply(this, args)
    }
  }
}

16.8 手写 instance API:myApply / myCall / myBind

Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx === null || ctx === undefined ? window : Object(ctx)
  const key = Symbol('fn')
  ctx[key] = this
  const res = ctx[key](...args)
  delete ctx[key]
  return res
}

Function.prototype.myApply = function (ctx, args = []) {
  return this.myCall(ctx, ...args)
}

Function.prototype.myBind = function (ctx, ...boundArgs) {
  const fn = this
  return function (...args) {
    return fn.myCall(this instanceof fn ? this : ctx, ...boundArgs, ...args)
  }
}

16.9 手写 new 操作符

function myNew(Ctor, ...args) {
  const obj = Object.create(Ctor.prototype)
  const res = Ctor.apply(obj, args)
  return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : obj
}

16.10 手写 instanceof

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left)
  while (proto) {
    if (proto === right.prototype) return true
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

16.11 手写发布订阅(与事件总线类似,常考)

参见 16.4 事件总线。

16.12 实现简单的 keep-alive(LRU 缓存思路)

Keep-Alive 的核心是缓存已渲染的组件 vnode,并根据 include/exclude 过滤,用 LRU 策略控制 max 数量:

// 伪代码思路
const cache = new Map() // key -> vnode
const keys = []         // LRU 队列

function pruneCacheEntry(key) {
  const cached = cache.get(key)
  if (cached) {
    cached.componentInstance?.$destroy() // Vue2 调用;Vue3 中 unmount
    cache.delete(key)
  }
}

// 渲染时
const key = getKey(vnode)
if (cache.has(key)) {
  vnode.componentInstance = cache.get(key).componentInstance
  // 移动到 keys 末尾(最近使用)
  keys.splice(keys.indexOf(key), 1)
  keys.push(key)
} else {
  cache.set(key, vnode)
  keys.push(key)
  if (max && keys.length > parseInt(max)) {
    pruneCacheEntry(keys[0]) // 删除最久未使用
    keys.shift()
  }
}

16.13 实现简易的 Promise

(篇幅较长,核心是状态机 + then 链 + 微任务调度,建议掌握标准 Promise/A+ 实现。)

十七、学习路径与面试准备建议

17.1 学习路径

  1. 基础阶段:HTML/CSS/JS 基础扎实 → Vue3 官方文档通读 → 用 Vite + Vue3 + Vue Router + Pinia 写一个完整项目(如 TodoList、博客后台)。
  2. 进阶阶段
    • 手写响应式、虚拟 DOM diff、发布订阅等基础实现。
    • 通读 Vue3 源码结构(reactivity、runtime-core、runtime-dom、compiler-core)。
    • 学习 TypeScript,用 TS 重构项目。
    • 掌握 Vite、ESLint、Prettier、Vitest 等工程化工具。
  3. 深入阶段
    • Nuxt3(SSR/SSG)、VueUse 源码、Element Plus/Naive UI 组件库原理。
    • 学习前端性能优化、浏览器原理、网络协议、工程化(Webpack/Vite/Rollup)。
    • 刷算法题(LeetCode HOT 100,重点:数组、字符串、链表、二叉树、动态规划、回溯)。

17.2 面试准备清单

  • 能讲清楚 Vue3 响应式流程(Proxy → track → trigger → effect),最好能写简化版。
  • 能对比 Options API vs Composition API、ref vs reactive、watch vs watchEffect、Vue2 vs Vue3。
  • 能说出 Vue3 Diff 算法改进点(PatchFlags、Block Tree、最长递增子序列)。
  • 能手写常见 JS 手写题(深拷贝、防抖节流、Promise、new、instanceof、事件总线、并发控制)。
  • 有完整的项目经验,能讲清楚项目架构、难点、性能优化、自己做了什么。
  • 准备好项目中的亮点(如自定义组件库、性能优化方案、SSR 实践、微前端接入、可视化大屏等)。
  • 刷近一年各大厂面经,重点关注:字节(原理深、算法重)、阿里(项目重、工程化)、腾讯(基础+项目)、美团(基础+场景题)。

17.3 推荐阅读与资源

  • Vue3 官方文档(cn.vuejs.org)——必读,尤其"深入响应式系统"、“渲染机制”、"最佳实践"章节。
  • Vue3 源码(github.com/vuejs/core)——从 packages/reactivity/src 开始读。
  • 《Vue.js 设计与实现》(霍春阳)——国产好书,原理讲得透彻。
  • VueUse 源码(github.com/vueuse/vueuse)——学习 Composable 最佳实践。
  • 小满 zs、Cobyte、阿崔cxr 等 B 站 UP 主的 Vue3 源码解析系列。

十八、常见场景题与加分项

场景 1:大型表单如何设计?

  • 用 reactive 聚合表单数据,配合 computed 做联动计算。
  • 用 async-validator 或 VeeValidate/zod 做校验。
  • 按业务模块拆分为子表单组件,通过 provide/inject 或 Pinia 共享表单状态。
  • 复杂联动用 watch 精确侦听关键字段,避免 deep watch 整表。
  • 表单数据可使用 shallowRef 包裹原始数据(如后端返回的大对象),提交时再处理。

场景 2:如何设计一个组件库?

  • Monorepo 架构(pnpm workspace)。
  • 统一设计 Token(颜色、间距、字号),CSS 变量支持主题切换。
  • 组件用 <script setup lang="ts"> + defineProps/defineEmits 类型声明。
  • 用 unplugin-vue-components 实现按需引入。
  • 用 Vite 库模式打包,输出 es + umd 格式。
  • 用 Vitest + Testing Library 做单元测试,用 Histoire/Histoire 或 VitePress 做文档。

场景 3:如何从 Vue2 升级到 Vue3?

  • 小项目:直接重写,使用 <script setup> + Pinia。
  • 大项目:使用 @vue/compat(兼容构建版本),按功能模块渐进迁移。
  • 先替换构建工具为 Vite(使用 vite-plugin-vue2 兼容 Vue2 项目后逐步迁移)。
  • Vue Router 3 → 4,Vuex → Pinia,UI 库替换为 Vue3 版本(Element Plus、Ant Design Vue、Naive UI)。
  • 迁移工具:@vue/composition-api 插件可先在 Vue2 项目中使用 Composition API 写法,降低迁移成本。

场景 4:Vue3 + TS 项目如何规范?

  • ESLint + Prettier + Stylelint 统一代码风格。
  • 使用 @vue/eslint-config-typescripteslint-plugin-vue(vue3-recommended 规则集)。
  • 约定 Composables 放在 composables/、Store 放在 stores/、API 请求放在 api/、类型定义放在 types/
  • 开启 strict: true(tsconfig),避免 any。
  • 使用 unplugin-auto-import 自动导入 ref、computed 等,减少样板代码但要配置好 eslint 识别。

本文从 API 使用到原理实现、从基础要点到面试真题,覆盖了 Vue3 进大厂必须掌握的核心内容。建议读者结合官方文档和源码阅读,多写多练,将知识点转化为项目实战经验。祝面试顺利,offer 拿到手软!