Vue3 基础必须掌握要点及大厂面试重点
本文系统梳理 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.use、Vue.mixin、Vue.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 最核心的变革,也是面试必考内容。它让相关逻辑可以组织在一起(按功能聚合),而非散落在 data、methods、computed、watch 等选项中。
3.1 setup 函数
setup 是 Composition API 的入口,在组件创建之前执行(在 beforeCreate 之前,此时组件实例尚未创建,this 为 undefined)。
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 的局限性(面试高频):
- 不能用于基本类型(string、number、boolean 等),只对对象类型有效。
- 不能整体替换:直接将响应式对象赋值给新变量会丢失响应性(断开引用)。
- 解构丢失响应性:解构 reactive 对象的属性后,变量是普通值,不是响应式的。解决方法:
- 使用
toRefs或toRef将属性转成 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 拦截读写。但存在以下问题:
- 无法监听新增/删除属性:需要
Vue.set/Vue.delete。 - 无法监听数组索引和 length:需要重写数组方法(push、pop、splice 等 7 个方法)。
- 初始化时递归遍历:对象嵌套深时性能差,且对 Map、Set、WeakMap 等新数据结构不支持。
4.2 Vue3 的 Proxy 方案
Proxy 可以代理整个对象,支持:
- 监听属性新增/删除(
set、deleteProperty) - 监听数组索引和 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 组件层面优化
- v-for 必须加 key:key 应为唯一 id,避免使用 index(列表增删时导致错误复用、状态错位)。
- v-if 与 v-for 不要同时用在同一元素:Vue3 中 v-if 优先级高于 v-for(与 Vue2 相反),可能导致意外行为;用
<template>包裹或先用 computed 过滤。 - 合理使用 v-once / v-memo:
v-once:只渲染一次,后续更新跳过。v-memo="[]":指定依赖数组,依赖不变则跳过子树更新(类似 React useMemo)。
- 大列表虚拟滚动:使用
vue-virtual-scroller、vueuc等虚拟列表方案。 - 组件懒加载:路由/组件使用
defineAsyncComponent按需加载。 - shallowRef / shallowReactive:大型不可变数据用浅层响应。
- markRaw:第三方类实例(如 ECharts、Three.js 对象)标记为非响应式。
- 冻结数据:
Object.freeze(data)或直接使用非响应式普通对象展示只读数据。
12.2 构建层面优化
- Tree-shaking:按需引入(如 Element Plus 的 unplugin-auto-import / unplugin-vue-components)。
- 代码分包:路由级、组件级动态 import。
- Gzip/Brotli 压缩:服务器开启。
- CDN 外链:Vue、VueRouter、Pinia 等可用 CDN,配合 externals 减小打包体积。
- 图片优化:WebP、懒加载、雪碧图、响应式图片。
- SSR / SSG:Nuxt3、ViteSSG 提升首屏和 SEO。
12.3 运行时优化
- 长列表防抖节流:滚动、输入等高频事件。
- computed 替代复杂模板表达式:缓存计算结果。
- watch 精准侦听:避免 deep: true 滥用,必要时手动指定侦听路径。
- 避免在模板中调用函数(除非事件处理):函数每次渲染都会执行,用 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 有哪些改进?为什么要重写?
答题框架:
- 响应式系统升级:Proxy 替代 defineProperty,解决数组、新增/删除属性问题,支持 Map/Set,惰性代理性能更好。
- Composition API:解决 Options API 中逻辑分散、Mixins 命名冲突/来源不清、TS 支持差的问题,提升大型项目可维护性和逻辑复用能力。
- 编译优化:PatchFlags、Block Tree、静态提升、事件缓存,运行时 diff 更精准。
- 体积更小:Tree-shaking 友好,核心运行时 ~10KB gzip。
- 性能更高:首次渲染快 55%,更新快 133%,内存少 54%。
- TS 原生支持:源码重写为 TS,类型推导完整。
- 新内置能力:Teleport、Suspense、Fragments、自定义元素。
- 架构调整: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"。 - 支持自定义修饰符:通过
modelModifiersprop 接收。 .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 借鉴了
ivi和inferno的算法,采用:- 头尾比对 + 最长递增子序列:先从头部和尾部比对相同节点,处理完新增/删除后,对中间乱序部分通过最长递增子序列算法求最小移动次数(比 Vue2 的递归双端对比更快)。
- PatchFlags 编译提示:diff 时只对比有 PatchFlag 标记的动态内容,跳过静态节点。
- Block Tree:将模板划分为块,块内用数组收集动态节点,diff 时直接遍历动态节点数组,不用递归全树。
Q18:为什么 v-for 必须用 key?用 index 作为 key 会有什么问题?
- key 是 vnode 的唯一标识,用于 diff 时判断节点是否可复用。
- 不用 key 时 Vue 采用就地复用策略(模式匹配),性能最好但状态可能错位。
- 用 index 作为 key 时,列表头部插入/删除元素会导致后续元素 key 全部变化,引发不必要的 DOM 更新和组件状态错位(如 input 值错位、动画错乱)。
- 正确做法:使用数据的唯一 id(如数据库主键)作为 key。
15.7 路由类
Q19:路由守卫有哪些?完整流程是什么?
全局守卫:beforeEach、beforeResolve、afterEach
路由独享:beforeEnter
组件内:beforeRouteEnter(Vue3 用 onBeforeRouteLeave/onBeforeRouteUpdate,没有 beforeRouteEnter 因为 setup 在 mount 之前执行)
完整导航解析流程(面试必须背诵):
- 导航触发
- 失活组件调用
beforeRouteLeave - 调用全局
beforeEach - 重用组件调用
beforeRouteUpdate - 路由配置
beforeEnter - 解析异步路由组件
- 激活组件调用
beforeRouteEnter(Vue3 Composition 中无对应) - 调用全局
beforeResolve - 导航确认
- 调用全局
afterEach - DOM 更新
beforeRouteEnter的 next 回调执行(Vue3 中用 onMounted 替代)
Q20:Hash 路由和 History 路由区别?History 部署刷新 404 怎么办?
- Hash 模式基于
window.location.hash和hashchange事件,# 后内容不发送给服务器,无需服务器配置,兼容性好但 URL 丑、SEO 差。 - History 模式基于 HTML5
history.pushState/replaceState和popstate事件,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 项目做过哪些性能优化?
从三个层面回答(参见第十二章):
- 编码层面:key 正确、v-if/v-for 不同用、v-once/v-memo、虚拟滚动、懒加载、shallowRef/markRaw、computed 缓存、防抖节流。
- 构建层面:路由分包、Tree-shaking、按需引入、Gzip、CDN、图片优化。
- 架构层面:SSR/SSG(Nuxt)、HTTP 缓存、Service Worker、预渲染。
Q24:Vue3 的 Tree-shaking 是怎么实现的?为什么 Vue2 做不到?
- Tree-shaking 依赖 ES Module 的静态结构(import/export 静态可分析)。
- Vue2 的全局 API 都挂载在 Vue 构造函数上(
Vue.nextTick、Vue.set、Vue.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 学习路径
- 基础阶段:HTML/CSS/JS 基础扎实 → Vue3 官方文档通读 → 用 Vite + Vue3 + Vue Router + Pinia 写一个完整项目(如 TodoList、博客后台)。
- 进阶阶段:
- 手写响应式、虚拟 DOM diff、发布订阅等基础实现。
- 通读 Vue3 源码结构(reactivity、runtime-core、runtime-dom、compiler-core)。
- 学习 TypeScript,用 TS 重构项目。
- 掌握 Vite、ESLint、Prettier、Vitest 等工程化工具。
- 深入阶段:
- 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-typescript、eslint-plugin-vue(vue3-recommended 规则集)。 - 约定 Composables 放在
composables/、Store 放在stores/、API 请求放在api/、类型定义放在types/。 - 开启
strict: true(tsconfig),避免 any。 - 使用 unplugin-auto-import 自动导入 ref、computed 等,减少样板代码但要配置好 eslint 识别。
本文从 API 使用到原理实现、从基础要点到面试真题,覆盖了 Vue3 进大厂必须掌握的核心内容。建议读者结合官方文档和源码阅读,多写多练,将知识点转化为项目实战经验。祝面试顺利,offer 拿到手软!