Vue.js 面试题整理
Vue.js 面试题整理
本文档系统整理了 Vue.js 常见面试题,涵盖 Vue2 和 Vue3 的核心知识点,包括基础概念、组件通信、生命周期、响应式原理、Composition API 等内容。每个问题都标注了难度等级(⭐),并提供详细的答案解析和代码示例。
📑 目录
一、Vue 基础概念
1.1 什么是前端构建工具
难度:⭐⭐
问题描述
什么是前端构建工具?比如 Vue2 的 webpack,Vue3 的 Vite。
答案解析
核心概念: 前端构建工具是帮助开发者将浏览器不认识的代码(TypeScript、Vue、Less 等)自动转换为浏览器可执行代码的工具。
背景说明:
- 浏览器只认识 HTML、CSS、JavaScript
- 现代项目使用 TypeScript、Vue/React、Less/Sass、ES6+ 等技术
- 需要各种编译工具:tsc、vue-compiler、less-loader、babel、uglifyjs 等
构建工具的核心功能:
-
模块化开发支持
- 支持从 node_modules 引入代码
- 支持多种模块化规范(CommonJS、ES Module 等)
-
代码兼容性处理
- TypeScript → JavaScript
- Vue/React → JavaScript
- Less/Sass → CSS
- ES6+ → ES5(通过 Babel)
-
性能优化
- 代码压缩(uglifyjs)
- 代码分割(Code Splitting)
- Tree Shaking(移除未使用代码)
-
开发体验优化
- 热更新(Hot Module Replacement)
- 开发服务器(解决跨域问题)
- 自动监听文件变化并重新编译
工作流程:
源代码变化 → 构建工具自动检测 → 调用对应编译工具 → 生成浏览器可执行代码 → 自动刷新浏览器
常见构建工具:
- Webpack:功能强大,配置复杂,适合大型项目
- Vite:基于 ES Module,开发时按需编译,启动速度快
- Rollup:适合库的打包
- Parcel:零配置,开箱即用
核心要点:
构建工具让开发者专注于业务逻辑,无需关心代码如何在浏览器运行,只需要写最舒服的代码即可。
1.2 MVVM 和 MVC 的原理及区别
难度:⭐⭐⭐
问题描述
简述 MVVM 和 MVC 的原理以及它们的区别。
答案解析
MVVM(Model-View-ViewModel)
核心特点: 视图模型双向绑定
组成部分:
- Model(模型):数据层,负责业务逻辑和数据
- View(视图):UI 层,负责页面展示
- ViewModel(视图模型):连接 Model 和 View,实现数据双向绑定
工作原理:
View ←→ ViewModel ←→ Model
双向绑定 数据处理
优点:
- 低耦合:View 可以独立于 Model 变化和修改
- 可重用性:一个 ViewModel 可以绑定到不同的 View
- 独立开发:前端专注 View,后端专注 Model
- 可测试性:ViewModel 易于单元测试
Vue 中的 MVVM:
<template>
<!-- View 视图层 -->
<div>{{ message }}</div>
<input v-model="message" />
</template>
<script>
export default {
data() {
// Model 数据层
return {
message: 'Hello Vue'
}
}
// Vue 实例充当 ViewModel
}
</script>
MVC(Model-View-Controller)
核心特点: 单向通信,通过 Controller 协调
组成部分:
- Model(模型):数据和业务逻辑(如数据库操作)
- View(视图):页面展示(HTML)
- Controller(控制器):处理用户交互,连接 Model 和 View
工作原理:
View → Controller → Model
↑ ↓
└───────────────────┘
单向通信
优点:
- 低耦合
- 重用性高
- 生命周期成本低
- 可维护性高
- 有利于软件工程化管理
MVVM 与 MVC 的区别
| 对比项 | MVC | MVVM |
|---|---|---|
| 通信方式 | 单向通信(View → Controller → Model) | 双向绑定(View ↔ ViewModel ↔ Model) |
| 核心角色 | Controller 处理业务逻辑 | ViewModel 实现数据绑定 |
| DOM 操作 | 需要手动操作 DOM | 自动同步,无需手动操作 |
| 数据流 | 必须通过 Controller 传递 | 数据自动同步 |
| 适用场景 | 传统 Web 应用 | 现代前端框架(Vue、React) |
核心要点:
MVVM 通过数据驱动视图,解决了 MVC 中大量 DOM 操作导致的性能问题,提升了开发效率和用户体验。
1.3 Vue 常见指令
难度:⭐⭐⭐
问题描述
列举 Vue 中常用的指令及其作用。
答案解析
数据绑定指令
1. v-model
- 作用:实现表单元素的双向数据绑定
- 适用:input、textarea、select 等表单元素
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
<script>
export default {
data() {
return {
message: 'Hello'
}
}
}
</script>
2. v-bind(简写 :)
- 作用:动态绑定属性
- 类型:字符串、对象、数组
<template>
<!-- 绑定属性 -->
<img :src="imageUrl" />
<!-- 绑定 class -->
<div :class="{ active: isActive }"></div>
<div :class="[classA, classB]"></div>
<!-- 绑定 style -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
</template>
事件绑定指令
3. v-on(简写 @)
- 作用:绑定事件监听器
<template>
<button @click="handleClick">点击</button>
<input @input="handleInput" />
<form @submit.prevent="handleSubmit">提交</form>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('clicked')
},
handleInput(e) {
console.log(e.target.value)
},
handleSubmit() {
// .prevent 阻止默认行为
}
}
}
</script>
条件渲染指令
4. v-if / v-else-if / v-else
- 作用:条件渲染,控制元素是否渲染到 DOM
- 特点:条件为 false 时,元素不会渲染
<template>
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
</template>
5. v-show
- 作用:控制元素显示/隐藏
- 特点:通过 CSS
display属性控制,元素始终渲染
<template>
<div v-show="isVisible">显示内容</div>
</template>
v-if vs v-show:
v-if:切换开销高,适合条件不频繁变化v-show:初始渲染开销高,适合频繁切换
列表渲染指令
6. v-for
- 作用:循环渲染列表
- 注意:必须添加唯一
key属性
<template>
<!-- 遍历数组 -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
<!-- 遍历对象 -->
<div v-for="(value, key, index) in object" :key="key">
{{ key }}: {{ value }}
</div>
</template>
其他常用指令
7. v-text
- 作用:更新元素的文本内容(等同于
{{ }})
<span v-text="message"></span>
<!-- 等同于 -->
<span>{{ message }}</span>
8. v-html
- 作用:渲染 HTML 内容
- 警告:谨防 XSS 攻击
<div v-html="htmlContent"></div>
9. v-once
- 作用:只渲染一次,后续不再更新
<span v-once>{{ message }}</span>
10. v-cloak
- 作用:防止页面闪烁(配合 CSS 使用)
<style>
[v-cloak] {
display: none;
}
</style>
<div v-cloak>{{ message }}</div>
11. v-pre
- 作用:跳过编译,原样输出
<span v-pre>{{ 这里不会被编译 }}</span>
核心要点:
Vue 指令是带有
v-前缀的特殊属性,用于在模板中声明式地绑定数据、事件和 DOM 操作。
1.4 data 为什么必须是函数
难度:⭐⭐⭐
问题描述
在 Vue 组件中,data 选项为什么必须是一个函数而不能是对象?
答案解析
核心原因: 保证每个组件实例拥有独立的数据对象,避免数据共享污染。
问题场景
如果 data 是对象(错误示例):
// ❌ 错误:所有实例共享同一个 data 对象
export default {
data: {
count: 0
}
}
问题: 当创建多个组件实例时,它们会共享同一个 data 对象,导致:
- 修改一个实例的数据,其他实例也会受影响
- 数据互相污染,无法隔离
正确做法
data 必须是函数:
// ✅ 正确:每个实例都有独立的 data 对象
export default {
data() {
return {
count: 0
}
}
}
原理: 每次创建组件实例时,都会调用 data 函数,返回一个新的数据对象,确保数据隔离。
完整示例
<!-- Counter.vue -->
<template>
<div>
<p>{{ message }}</p>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script>
export default {
data() {
// 每个实例调用时都会返回新对象
return {
message: 'Hello, Vue!',
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<!-- App.vue -->
<template>
<div>
<!-- 三个独立的 Counter 实例,数据互不影响 -->
<Counter />
<Counter />
<Counter />
</div>
</template>
<script>
import Counter from './Counter.vue'
export default {
components: { Counter }
}
</script>
特殊情况
根实例可以使用对象:
// 根实例只会创建一次,可以使用对象
new Vue({
data: {
count: 0
}
})
函数参数的高级用法
data 函数可以接收组件实例作为参数:
export default {
props: ['initialCount'],
data(vm) {
// vm 是组件实例
return {
count: vm.initialCount || 0
}
}
}
核心要点:
data 必须是函数,确保每个组件实例都有独立的数据副本,这是 Vue 组件化设计的基础。
1.5 ref 的作用
难度:⭐⭐⭐
问题描述
Vue 中 ref 的作用是什么?如何使用?
答案解析
核心作用: ref 用于获取 DOM 元素或子组件实例的引用。
使用场景
1. 获取 DOM 元素引用
<template>
<div>
<div ref="testDom">DOM 元素</div>
<button @click="getElement">获取元素</button>
</div>
</template>
<script>
export default {
methods: {
getElement() {
// 通过 this.$refs 访问
console.log(this.$refs.testDom)
// 可以直接操作 DOM
this.$refs.testDom.style.color = 'red'
}
}
}
</script>
2. 获取子组件实例
<!-- 父组件 -->
<template>
<div>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
mounted() {
// 访问子组件的数据
console.log(this.$refs.childRef.message)
// 调用子组件的方法
this.$refs.childRef.sayHello()
},
methods: {
callChildMethod() {
this.$refs.childRef.sayHello()
}
}
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>子组件</div>
</template>
<script>
export default {
data() {
return {
message: '来自子组件的数据'
}
},
methods: {
sayHello() {
console.log('Hello from child!')
}
}
}
</script>
3. 在 v-for 中使用 ref
<template>
<div>
<div
v-for="(item, index) in items"
:key="item.id"
:ref="'item' + index"
>
{{ item.name }}
</div>
<button @click="getItems">获取所有元素</button>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
}
},
methods: {
getItems() {
// ref 在 v-for 中会是一个数组
this.items.forEach((item, index) => {
console.log(this.$refs['item' + index])
})
}
}
}
</script>
Vue3 中的 ref
在 Vue3 Composition API 中,ref 的使用方式有所不同:
<template>
<div>
<div ref="elementRef">DOM 元素</div>
<Child ref="childRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 声明 ref(初始值为 null)
const elementRef = ref(null)
const childRef = ref(null)
onMounted(() => {
// 组件挂载后才能访问
console.log(elementRef.value) // DOM 元素
console.log(childRef.value) // 子组件实例
})
</script>
注意事项
1. 访问时机
- ref 需要在 DOM 渲染完成后才能访问
- 在
mounted生命周期或onMounted中使用 - 或在
this.$nextTick()中使用
export default {
mounted() {
// ✅ 正确:DOM 已渲染
console.log(this.$refs.myElement)
},
created() {
// ❌ 错误:DOM 未渲染
console.log(this.$refs.myElement) // undefined
}
}
2. v-for 中的 ref
- 如果 ref 用在 v-for 中,
$refs会是一个数组 - 需要遍历访问
3. 响应式注意
- ref 引用本身不是响应式的
- 不要在模板中依赖 ref 的值
核心要点:
ref 提供了直接访问 DOM 和组件实例的能力,但应谨慎使用,优先使用数据驱动的方式。
二、组件通信
2.1 Vue2 组件通信方式
难度:⭐⭐⭐⭐⭐
问题描述
Vue2 中组件之间有哪些通信方式?
答案解析
1. 父传子:Props
父组件:
<!-- App.vue -->
<template>
<div id="app">
<Child :users="users" :count="count" />
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'App',
data() {
return {
users: ['Alice', 'Bob', 'Charlie'],
count: 100
}
},
components: {
Child
}
}
</script>
子组件:
<!-- Child.vue -->
<template>
<div class="child">
<ul>
<li v-for="user in users" :key="user">{{ user }}</li>
</ul>
<p>Count: {{ count }}</p>
</div>
</template>
<script>
export default {
name: 'Child',
props: {
users: {
type: Array,
required: true
},
count: {
type: Number,
default: 0
}
}
}
</script>
2. 子传父:$emit
子组件:
<!-- Header.vue -->
<template>
<header>
<h1 @click="changeTitle">{{ title }}</h1>
</header>
</template>
<script>
export default {
name: 'Header',
data() {
return {
title: 'Vue.js Demo'
}
},
methods: {
changeTitle() {
// 触发自定义事件,传递数据
this.$emit('titleChanged', '子组件传递的新标题')
}
}
}
</script>
父组件:
<!-- App.vue -->
<template>
<div id="app">
<!-- 监听子组件的自定义事件 -->
<Header @titleChanged="updateTitle" />
<h2>{{ title }}</h2>
</div>
</template>
<script>
import Header from './components/Header.vue'
export default {
name: 'App',
data() {
return {
title: '初始标题'
}
},
methods: {
updateTitle(newTitle) {
// 接收子组件传递的数据
this.title = newTitle
}
},
components: {
Header
}
}
</script>
3. 兄弟组件通信:事件总线(EventBus)
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
<!-- ComponentA.vue -->
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script>
import { EventBus } from './eventBus.js'
export default {
methods: {
sendMessage() {
EventBus.$emit('message', 'Hello from A')
}
}
}
</script>
<!-- ComponentB.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
import { EventBus } from './eventBus.js'
export default {
data() {
return {
message: ''
}
},
mounted() {
EventBus.$on('message', (msg) => {
this.message = msg
})
},
beforeDestroy() {
// 组件销毁前移除监听
EventBus.$off('message')
}
}
</script>
4. 跨层级通信:provide / inject
<!-- 祖先组件 -->
<script>
export default {
provide() {
return {
theme: 'dark',
user: this.currentUser
}
},
data() {
return {
currentUser: { name: '张三' }
}
}
}
</script>
<!-- 后代组件(任意层级) -->
<script>
export default {
inject: ['theme', 'user'],
mounted() {
console.log(this.theme) // 'dark'
console.log(this.user) // { name: '张三' }
}
}
</script>
5. 通过 $parent / $children 访问
// 子组件访问父组件
this.$parent.parentMethod()
this.$parent.parentData
// 父组件访问子组件
this.$children[0].childMethod()
this.$children[0].childData
注意: 不推荐使用,耦合度高,难以维护。
核心要点:
Vue2 提供了多种组件通信方式,应根据组件关系选择合适的方法:父子用 props/$emit,跨层级用 provide/inject,全局状态用 Vuex。
2.2 Vuex 的理解及使用场景
难度:⭐⭐⭐
问题描述
什么是 Vuex?它的核心概念有哪些?适用于什么场景?
答案解析
核心概念: Vuex 是 Vue 应用程序的状态管理模式,采用集中式存储管理应用的所有组件状态。
Vuex 的核心组成
1. State(状态)
- 定义应用的状态数据
- 单一状态树,唯一数据源
const store = new Vuex.Store({
state: {
count: 0,
user: {
name: '张三',
age: 20
}
}
})
// 组件中访问
this.$store.state.count
2. Getter(计算属性)
- 从 state 派生出的状态
- 类似于组件的计算属性
- 结果会被缓存
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '学习 Vue', done: true },
{ id: 2, text: '学习 Vuex', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
})
// 组件中访问
this.$store.getters.doneTodos
3. Mutation(同步修改状态)
- 唯一修改 state 的方法
- 必须是同步函数
- 通过 commit 触发
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
},
incrementBy(state, payload) {
state.count += payload.amount
}
}
})
// 组件中调用
this.$store.commit('increment')
this.$store.commit('incrementBy', { amount: 10 })
4. Action(异步操作)
- 提交 mutation,而不是直接变更状态
- 可以包含任意异步操作
- 通过 dispatch 触发
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
},
// 异步请求示例
async fetchUser({ commit }, userId) {
const response = await fetch(`/api/user/${userId}`)
const user = await response.json()
commit('setUser', user)
}
}
})
// 组件中调用
this.$store.dispatch('incrementAsync')
5. Module(模块化)
- 将 store 分割成模块
- 每个模块拥有自己的 state、mutation、action、getter
const moduleA = {
state: () => ({ count: 0 }),
mutations: {
increment(state) {
state.count++
}
},
actions: {},
getters: {}
}
const moduleB = {
state: () => ({ count: 0 }),
mutations: {},
actions: {}
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
// 访问模块状态
store.state.a.count
store.state.b.count
完整示例
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
todos: []
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
},
mutations: {
increment(state) {
state.count++
},
addTodo(state, todo) {
state.todos.push(todo)
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
modules: {}
})
<!-- 组件中使用 -->
<template>
<div>
<p>Count: {{ count }}</p>
<p>Done Todos: {{ doneTodos.length }}</p>
<button @click="increment">+1</button>
<button @click="incrementAsync">异步+1</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 映射 state
...mapState(['count', 'todos']),
// 映射 getters
...mapGetters(['doneTodos'])
},
methods: {
// 映射 mutations
...mapMutations(['increment']),
// 映射 actions
...mapActions(['incrementAsync'])
}
}
</script>
使用场景
适合使用 Vuex:
- 多个组件共享状态
- 多个组件需要修改同一状态
- 需要跨页面保持状态
- 复杂的状态管理逻辑
- 中大型单页应用
不适合使用 Vuex:
- 简单的父子组件通信(用 props/emit)
- 小型应用(状态管理简单)
- 临时性的状态(用组件内部 data)
核心要点:
Vuex 通过集中式状态管理,解决了多组件共享状态的问题。核心原则是:State 只能通过 Mutation 修改,异步操作放在 Action 中。
2.3 Vue3 组件通信方式
难度:⭐⭐⭐⭐⭐
问题描述
Vue3 中组件通信方式有哪些?与 Vue2 有什么区别?
答案解析
Vue3 在保留 Vue2 通信方式的基础上,新增了 Composition API 相关的方法。
1. 父传子:Props(defineProps)
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMsg" :user="userInfo" />
</template>
<script setup>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentMsg = ref('来自父组件的消息')
const userInfo = reactive({ name: '张三', age: 20 })
</script>
<!-- 子组件 -->
<template>
<div>
<p>{{ message }}</p>
<p>{{ user.name }} - {{ user.age }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
// 方式1:简单声明
const props = defineProps(['message', 'user'])
// 方式2:带类型校验(推荐)
const props = defineProps({
message: {
type: String,
required: true
},
user: {
type: Object,
default: () => ({})
}
})
// 使用 props(无需 .value)
console.log(props.message)
</script>
2. 子传父:Emits(defineEmits)
<!-- 子组件 -->
<template>
<button @click="handleClick">发送数据</button>
</template>
<script setup>
import { defineEmits } from 'vue'
// 定义可触发的事件
const emit = defineEmits(['sendData', 'update'])
// 或带类型校验
const emit = defineEmits({
sendData: (data) => typeof data === 'string',
update: (num) => num > 0
})
const handleClick = () => {
emit('sendData', '来自子组件的消息')
emit('update', 100)
}
</script>
<!-- 父组件 -->
<template>
<ChildComponent
@sendData="onReceive"
@update="onUpdate"
/>
</template>
<script setup>
const onReceive = (data) => {
console.log('收到子组件数据:', data)
}
const onUpdate = (num) => {
console.log('收到更新值:', num)
}
</script>
3. 父子双向绑定:v-model
单值绑定:
<!-- 父组件 -->
<template>
<ChildComponent v-model="count" />
<!-- 等价于 <ChildComponent :modelValue="count" @update:modelValue="count = $event" /> -->
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- 子组件 -->
<template>
<button @click="handleAdd">count: {{ modelValue }}</button>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleAdd = () => {
emit('update:modelValue', props.modelValue + 1)
}
</script>
多值绑定:
<!-- 父组件 -->
<template>
<ChildComponent
v-model:name="userName"
v-model:age="userAge"
/>
</template>
<script setup>
import { ref } from 'vue'
const userName = ref('张三')
const userAge = ref(20)
</script>
<!-- 子组件 -->
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps(['name', 'age'])
const emit = defineEmits(['update:name', 'update:age'])
const updateName = (newName) => {
emit('update:name', newName)
}
const updateAge = (newAge) => {
emit('update:age', newAge)
}
</script>
4. 跨层级通信:Provide / Inject
<!-- 顶层组件(提供数据) -->
<script setup>
import { provide, ref } from 'vue'
// 提供普通值
provide('theme', 'dark')
// 提供响应式数据(子组件会同步更新)
const user = ref({ name: '张三' })
provide('currentUser', user)
// 提供修改方法
const updateUser = (newName) => {
user.value.name = newName
}
provide('updateUser', updateUser)
</script>
<!-- 深层子组件(接收数据) -->
<template>
<div>
<p>Theme: {{ theme }}</p>
<p>User: {{ currentUser.name }}</p>
<button @click="updateUser('李四')">更新用户</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 接收数据(第二个参数为默认值)
const theme = inject('theme', 'light')
const currentUser = inject('currentUser')
const updateUser = inject('updateUser')
</script>
5. 全局状态管理:Pinia
Vue3 推荐使用 Pinia 替代 Vuex。
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: { name: '张三' }
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchUser() {
const response = await fetch('/api/user')
this.user = await response.json()
}
}
})
<!-- 任意组件使用 -->
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
<script setup>
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
// 访问状态
console.log(counter.count)
// 修改状态(直接修改)
counter.count++
// 或调用 action
counter.increment()
</script>
6. 通过 ref 访问子组件
<!-- 父组件 -->
<template>
<Child ref="childRef" />
<button @click="callChild">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChild = () => {
// 访问子组件暴露的方法和数据
childRef.value.childMethod()
console.log(childRef.value.childData)
}
</script>
<!-- 子组件 -->
<script setup>
import { ref, defineExpose } from 'vue'
const childData = ref('子组件数据')
const childMethod = () => {
console.log('子组件方法被调用')
}
// 必须显式暴露给父组件
defineExpose({
childData,
childMethod
})
</script>
Vue2 vs Vue3 通信方式对比
| 通信方式 | Vue2 | Vue3 |
|---|---|---|
| Props | props 选项 | defineProps() |
| Emits | $emit() | defineEmits() |
| v-model | 单值绑定 | 支持多值绑定 |
| Provide/Inject | provide/inject 选项 | provide()/inject() |
| 状态管理 | Vuex | Pinia(推荐) |
| 访问子组件 | $refs | ref + defineExpose |
| 事件总线 | EventBus | mitt 库 |
核心要点:
Vue3 通过 Composition API 提供了更灵活的组件通信方式,推荐使用 defineProps、defineEmits、provide/inject 和 Pinia 进行状态管理。
三、生命周期
3.1 Vue2 生命周期八个阶段
难度:⭐⭐⭐⭐
问题描述
Vue2 的生命周期有哪八个阶段?每个阶段的特点是什么?
答案解析
Vue2 组件从创建到销毁经历 8 个生命周期钩子函数。
创建阶段
1. beforeCreate(创建前)
- 时机:实例初始化之后,数据观测和事件配置之前
- 特点:
- data、methods、computed、watch 都不可用
- 只有生命周期函数和默认事件
- 用途:很少使用,一般用于插件开发
export default {
beforeCreate() {
console.log('beforeCreate')
console.log(this.message) // undefined
console.log(this.getData) // undefined
}
}
2. created(创建后)
- 时机:实例创建完成,数据观测、属性和方法的运算已完成
- 特点:
- data、methods、computed、watch 已初始化
- DOM 还未生成,$el 不可用
- 用途:
- 调用 methods 中的方法
- 操作 data 中的数据
- 发起异步请求(推荐)
export default {
data() {
return {
message: 'Hello'
}
},
created() {
console.log('created')
console.log(this.message) // 'Hello'
// 适合发起 API 请求
this.fetchData()
},
methods: {
fetchData() {
// 发起请求
}
}
}
挂载阶段
3. beforeMount(挂载前)
- 时机:模板编译完成,虚拟 DOM 已创建,但未挂载到页面
- 特点:
- 模板已在内存中编译
- 页面还未渲染
- $el 已存在但是旧的
- 用途:很少使用
export default {
beforeMount() {
console.log('beforeMount')
console.log(this.$el) // 存在但未更新
}
}
4. mounted(挂载后)
- 时机:实例挂载到 DOM,页面渲染完成
- 特点:
- DOM 已渲染完成
- 可以访问 DOM 元素
- 可以操作 $refs
- 用途:
- 操作 DOM
- 使用第三方 DOM 库
- 发起异步请求(也可以)
export default {
mounted() {
console.log('mounted')
// 可以访问 DOM
console.log(this.$refs.myElement)
// 可以操作 DOM
this.$refs.myElement.style.color = 'red'
}
}
更新阶段
5. beforeUpdate(更新前)
- 时机:数据更新时调用,发生在虚拟 DOM 重新渲染之前
- 特点:
- data 中的数据是最新的
- 页面显示的数据是旧的
- 页面和数据尚未同步
- 用途:
- 在更新前访问现有的 DOM
- 移除手动添加的事件监听器
export default {
data() {
return {
count: 0
}
},
beforeUpdate() {
console.log('beforeUpdate')
console.log('data:', this.count) // 最新值
console.log('DOM:', this.$refs.count.textContent) // 旧值
}
}
6. updated(更新后)
- 时机:数据更改导致的虚拟 DOM 重新渲染和打补丁之后
- 特点:
- data 和页面都是最新的
- DOM 已更新完成
- 用途:
- 执行依赖于 DOM 的操作
- 注意:避免在此更改状态,可能导致无限循环
export default {
updated() {
console.log('updated')
console.log('data:', this.count) // 最新值
console.log('DOM:', this.$refs.count.textContent) // 最新值
// ❌ 避免在这里修改数据
// this.count++ // 会导致无限循环
}
}
销毁阶段
7. beforeDestroy(销毁前)
- 时机:实例销毁之前调用
- 特点:
- 实例仍然完全可用
- data、methods 等都可用
- 用途:
- 清除定时器
- 解绑自定义事件
- 取消网络请求
- 清理手动创建的 DOM
export default {
data() {
return {
timer: null
}
},
mounted() {
this.timer = setInterval(() => {
console.log('tick')
}, 1000)
},
beforeDestroy() {
console.log('beforeDestroy')
// 清除定时器
if (this.timer) {
clearInterval(this.timer)
}
// 解绑事件
window.removeEventListener('resize', this.handleResize)
}
}
8. destroyed(销毁后)
- 时机:实例销毁后调用
- 特点:
- 所有指令解绑
- 所有事件监听器移除
- 所有子实例销毁
- data、methods 等不可用
- 用途:很少使用
export default {
destroyed() {
console.log('destroyed')
// 组件已完全销毁
}
}
生命周期流程图
创建阶段:
new Vue()
↓
beforeCreate → 初始化事件和生命周期
↓
created → 初始化 data、methods、computed、watch
↓
挂载阶段:
beforeMount → 编译模板,生成虚拟 DOM
↓
mounted → 挂载到真实 DOM
↓
更新阶段(数据变化时):
beforeUpdate → 数据更新,虚拟 DOM 重新渲染前
↓
updated → 虚拟 DOM 重新渲染并打补丁后
↓
销毁阶段:
beforeDestroy → 实例销毁前
↓
destroyed → 实例销毁后
核心要点:
Vue2 生命周期分为创建、挂载、更新、销毁四个阶段。created 适合数据初始化和 API 请求,mounted 适合 DOM 操作,beforeDestroy 适合清理工作。
3.2 生命周期的使用场景
难度:⭐⭐⭐
问题描述
Vue 生命周期各个阶段分别适合做什么?
答案解析
beforeCreate
- 适用场景:很少使用
- 可以做:
- 加载 loading 事件
- 插件开发时的初始化
- 不能做:
- 访问 data、methods(还未初始化)
created
- 适用场景:最常用的初始化阶段
- 可以做:
- ✅ 发起 API 请求(推荐)
- ✅ 操作 data 中的数据
- ✅ 调用 methods 中的方法
- ✅ 初始化非响应式变量
- 不能做:
- ❌ 操作 DOM(还未渲染)
- ❌ 访问 $refs(还未挂载)
export default {
data() {
return {
userList: []
}
},
created() {
// ✅ 推荐:发起 API 请求
this.fetchUserList()
},
methods: {
async fetchUserList() {
const res = await fetch('/api/users')
this.userList = await res.json()
}
}
}
beforeMount
- 适用场景:很少使用
- 可以做:
- 在渲染前最后一次修改数据
- 不能做:
- 操作 DOM(还未挂载)
mounted
- 适用场景:需要操作 DOM 时
- 可以做:
- ✅ 操作 DOM 元素
- ✅ 访问 $refs
- ✅ 使用第三方 DOM 库(如 ECharts、Swiper)
- ✅ 发起 API 请求(也可以,但推荐在 created)
- ✅ 添加事件监听器
export default {
mounted() {
// ✅ 操作 DOM
this.$refs.input.focus()
// ✅ 使用第三方库
this.chart = echarts.init(this.$refs.chartContainer)
// ✅ 添加事件监听
window.addEventListener('resize', this.handleResize)
}
}
beforeUpdate
- 适用场景:很少使用
- 可以做:
- 在更新前访问现有的 DOM
- 移除手动添加的事件监听器
- 注意:
- 不要在这里修改数据(会再次触发更新)
updated
- 适用场景:需要在 DOM 更新后执行操作
- 可以做:
- ✅ 执行依赖于 DOM 的操作
- ✅ 访问更新后的 DOM
- 不能做:
- ❌ 修改数据(会导致无限循环)
export default {
data() {
return {
scrollToBottom: false
}
},
updated() {
// ✅ DOM 更新后滚动到底部
if (this.scrollToBottom) {
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight
this.scrollToBottom = false
}
}
}
beforeDestroy
- 适用场景:组件销毁前的清理工作(最重要)
- 可以做:
- ✅ 清除定时器
- ✅ 解绑自定义事件
- ✅ 取消网络请求
- ✅ 移除事件监听器
- ✅ 清理手动创建的 DOM
export default {
data() {
return {
timer: null,
eventBus: null
}
},
mounted() {
// 创建定时器
this.timer = setInterval(() => {
this.updateTime()
}, 1000)
// 添加事件监听
window.addEventListener('resize', this.handleResize)
// 监听事件总线
this.eventBus = EventBus.$on('update', this.handleUpdate)
},
beforeDestroy() {
// ✅ 清除定时器
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
// ✅ 移除事件监听
window.removeEventListener('resize', this.handleResize)
// ✅ 解绑事件总线
EventBus.$off('update', this.handleUpdate)
}
}
destroyed
- 适用场景:很少使用
- 可以做:
- 确认清理完成
- 发送销毁统计
常见使用场景总结
| 场景 | 推荐生命周期 | 原因 |
|---|---|---|
| 发起 API 请求 | created | 越早获取数据越好,减少白屏时间 |
| 操作 DOM | mounted | DOM 已渲染完成 |
| 使用第三方 DOM 库 | mounted | 需要真实 DOM 元素 |
| 清除定时器 | beforeDestroy | 防止内存泄漏 |
| 移除事件监听 | beforeDestroy | 防止内存泄漏 |
| 初始化数据 | created | data 已初始化 |
| 访问 $refs | mounted | 组件已挂载 |
核心要点:
created 适合数据初始化和 API 请求,mounted 适合 DOM 操作,beforeDestroy 必须做清理工作防止内存泄漏。
3.3 Vue2 和 Vue3 生命周期对比
难度:⭐⭐⭐⭐⭐
问题描述
Vue2 和 Vue3 的生命周期有什么区别?
答案解析
Options API 对比
Vue3 保留了 Vue2 的生命周期钩子,但有两个重命名:
| Vue2 | Vue3 | 说明 |
|---|---|---|
| beforeCreate | beforeCreate | 创建前 |
| created | created | 创建后 |
| beforeMount | beforeMount | 挂载前 |
| mounted | mounted | 挂载后 |
| beforeUpdate | beforeUpdate | 更新前 |
| updated | updated | 更新后 |
| beforeDestroy | beforeUnmount | ⚠️ 重命名:卸载前 |
| destroyed | unmounted | ⚠️ 重命名:卸载后 |
<!-- Vue3 Options API -->
<script>
export default {
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() { // ⚠️ Vue2 中是 beforeDestroy
console.log('beforeUnmount')
},
unmounted() { // ⚠️ Vue2 中是 destroyed
console.log('unmounted')
}
}
</script>
Composition API 生命周期
Vue3 新增了 Composition API 形式的生命周期钩子:
| Options API | Composition API | 说明 |
|---|---|---|
| beforeCreate | ❌ 不需要 | 使用 setup() 替代 |
| created | ❌ 不需要 | 使用 setup() 替代 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeUnmount | onBeforeUnmount | 卸载前 |
| unmounted | onUnmounted | 卸载后 |
特殊说明:
setup()在beforeCreate和created之间执行setup()中可以直接初始化数据,无需beforeCreate和created
<!-- Vue3 Composition API -->
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
// setup() 阶段(相当于 beforeCreate + created)
const count = ref(0)
console.log('setup 阶段')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
// 操作 DOM
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
// 清理工作
})
onUnmounted(() => {
console.log('onUnmounted')
})
</script>
完整对比示例
Vue2 写法:
<script>
export default {
data() {
return {
count: 0,
timer: null
}
},
created() {
console.log('created')
this.fetchData()
},
mounted() {
console.log('mounted')
this.timer = setInterval(() => {
this.count++
}, 1000)
},
beforeDestroy() {
console.log('beforeDestroy')
if (this.timer) {
clearInterval(this.timer)
}
},
methods: {
fetchData() {
// 请求数据
}
}
}
</script>
Vue3 Composition API 写法:
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
// setup 阶段(相当于 created)
const count = ref(0)
let timer = null
console.log('setup 阶段')
fetchData()
onMounted(() => {
console.log('onMounted')
timer = setInterval(() => {
count.value++
}, 1000)
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
if (timer) {
clearInterval(timer)
}
})
function fetchData() {
// 请求数据
}
</script>
执行顺序
Vue3 Composition API 执行顺序:
setup()
↓
onBeforeMount()
↓
onMounted()
↓
onBeforeUpdate() (数据变化时)
↓
onUpdated() (数据变化时)
↓
onBeforeUnmount()
↓
onUnmounted()
核心要点:
Vue3 将 beforeDestroy/destroyed 重命名为 beforeUnmount/unmounted。Composition API 中,setup() 替代了 beforeCreate 和 created,其他生命周期需要从 vue 导入并以 on 开头。
四、响应式原理
4.1 Vue2 和 Vue3 响应式原理对比
难度:⭐⭐⭐⭐⭐
问题描述
Vue2 和 Vue3 的响应式原理有什么区别?各自的优缺点是什么?
答案解析
Vue2 响应式原理(Object.defineProperty)
实现方式: 基于 ES5 的 Object.defineProperty 实现
工作流程:
- 初始化时:递归遍历 data 对象的所有属性
- 添加 getter:收集依赖(记录哪些组件使用了这个属性)
- 添加 setter:通知依赖更新(数据变化时通知相关组件重新渲染)
核心代码示例:
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`访问了 ${key}`)
// 收集依赖
track(obj, key)
return val
},
set(newVal) {
if (newVal === val) return
console.log(`修改了 ${key}`)
val = newVal
// 通知更新
trigger(obj, key)
}
})
}
// 使用示例
const data = { count: 0 }
defineReactive(data, 'count', data.count)
data.count // 触发 getter
data.count = 1 // 触发 setter
局限性:
- 无法监听对象新增/删除属性
const data = { name: '张三' }
// ❌ 新增属性不会触发更新
data.age = 20
// ✅ 需要使用 Vue.set
Vue.set(data, 'age', 20)
// 或
this.$set(data, 'age', 20)
- 无法监听数组索引修改
const arr = [1, 2, 3]
// ❌ 通过索引修改不会触发更新
arr[0] = 100
// ✅ 需要使用数组方法
arr.splice(0, 1, 100)
// 或使用 Vue.set
Vue.set(arr, 0, 100)
- 初始化性能问题
- 需要递归遍历所有属性
- 大对象初始化时性能损耗明显
Vue3 响应式原理(Proxy)
实现方式: 基于 ES6 的 Proxy 实现
工作流程:
- 创建代理:用 Proxy 代理整个对象
- 拦截操作:拦截对象的所有操作(get、set、has、deleteProperty 等)
- 懒代理:只有访问属性时才递归处理嵌套对象
核心代码示例:
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
console.log(`访问了 ${key}`)
const result = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
// 懒代理:只有访问时才递归
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`修改了 ${key}`)
const result = Reflect.set(target, key, value, receiver)
// 通知更新
trigger(target, key)
return result
},
deleteProperty(target, key) {
console.log(`删除了 ${key}`)
const result = Reflect.deleteProperty(target, key)
// 通知更新
trigger(target, key)
return result
}
})
}
// 使用示例
const data = reactive({ count: 0 })
data.count // 触发 get
data.count = 1 // 触发 set
delete data.count // 触发 deleteProperty
优势:
- 原生支持新增/删除属性
const data = reactive({ name: '张三' })
// ✅ 新增属性会触发更新
data.age = 20
// ✅ 删除属性会触发更新
delete data.age
- 完美支持数组
const arr = reactive([1, 2, 3])
// ✅ 索引修改会触发更新
arr[0] = 100
// ✅ 长度修改会触发更新
arr.length = 0
- 性能更优
- 懒代理:只有访问时才处理嵌套对象
- 不需要初始化时递归遍历所有属性
- 支持更多数据结构
// ✅ 支持 Map
const map = reactive(new Map())
map.set('key', 'value')
// ✅ 支持 Set
const set = reactive(new Set())
set.add(1)
对比总结
| 对比项 | Vue2 (Object.defineProperty) | Vue3 (Proxy) |
|---|---|---|
| 新增属性 | ❌ 不支持,需要 $set | ✅ 原生支持 |
| 删除属性 | ❌ 不支持,需要 $delete | ✅ 原生支持 |
| 数组索引 | ❌ 不支持 | ✅ 原生支持 |
| 数组长度 | ❌ 不支持 | ✅ 原生支持 |
| Map/Set | ❌ 不支持 | ✅ 原生支持 |
| 初始化性能 | ❌ 需要递归遍历 | ✅ 懒代理,按需处理 |
| 浏览器兼容 | ✅ IE9+ | ❌ IE 不支持 |
实际使用对比
Vue2:
<script>
export default {
data() {
return {
user: {
name: '张三'
},
list: [1, 2, 3]
}
},
methods: {
addAge() {
// ❌ 不会触发更新
this.user.age = 20
// ✅ 正确做法
this.$set(this.user, 'age', 20)
},
updateList() {
// ❌ 不会触发更新
this.list[0] = 100
// ✅ 正确做法
this.$set(this.list, 0, 100)
// 或
this.list.splice(0, 1, 100)
}
}
}
</script>
Vue3:
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: '张三'
})
const list = reactive([1, 2, 3])
function addAge() {
// ✅ 直接添加,会触发更新
user.age = 20
}
function updateList() {
// ✅ 直接修改,会触发更新
list[0] = 100
}
</script>
核心要点:
Vue2 使用 Object.defineProperty 实现响应式,存在无法监听新增/删除属性和数组索引修改的问题。Vue3 使用 Proxy 实现,完美解决了这些问题,性能更优,但不支持 IE 浏览器。
五、Vue3 新特性
5.1 Vue2 和 Vue3 的核心区别
难度:⭐⭐⭐⭐
问题描述
Vue2 和 Vue3 有哪些核心区别?为什么要升级到 Vue3?
答案解析
1. 响应式机制不同
Vue2: Object.defineProperty
- 无法监听对象新增/删除属性
- 无法监听数组索引修改
- 初始化时需要递归遍历所有属性
Vue3: Proxy
- 原生支持对象新增/删除属性
- 完美支持数组操作
- 懒代理,性能更优
// Vue2
this.$set(this.user, 'age', 20) // 需要使用 $set
// Vue3
user.age = 20 // 直接添加即可
2. 代码组织方式
Vue2: Options API(选项式 API)
- 代码按 data、methods、computed 等选项划分
- 相关逻辑分散在不同选项中
- 难以复用逻辑
<script>
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
},
computed: {
double() { return this.count * 2 }
}
}
</script>
Vue3: Composition API(组合式 API)
- 按业务逻辑组织代码
- 相关逻辑聚合在一起
- 易于复用和维护
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const increment = () => count.value++
const double = computed(() => count.value * 2)
</script>
3. 性能优化
编译优化:
- 静态提升:静态节点只创建一次,后续复用
- 事件缓存:事件处理函数缓存,避免重复创建
- Patch Flag:标记动态节点,更新时只处理动态内容
<!-- Vue3 编译优化 -->
<template>
<div>
<p>静态文本</p> <!-- 静态提升,只创建一次 -->
<p>{{ count }}</p> <!-- 标记为动态节点 -->
</div>
</template>
体积优化:
- Tree-shaking:未使用的 API 不会打包
- 核心库体积减少约 40%
// Vue3 按需引入
import { ref, computed } from 'vue'
// 未使用的 watch、reactive 等不会打包
渲染优化:
- Fragment:支持多根节点,减少不必要的 DOM 嵌套
- Teleport:将组件渲染到 DOM 的其他位置
- Suspense:异步组件加载状态管理
<!-- Vue3 支持多根节点 -->
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template>
4. TypeScript 支持
Vue2:
- TypeScript 支持不完善
- 需要额外配置
Vue3:
- 源码用 TypeScript 重写
- 类型定义更完善
- 开发时类型提示更友好
// Vue3 TypeScript 支持
import { ref, Ref } from 'vue'
const count: Ref<number> = ref(0)
const message: Ref<string> = ref('Hello')
5. 新增特性
Composition API:
<script setup>
import { ref, reactive, computed, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
const double = computed(() => count.value * 2)
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
</script>
Teleport(传送门):
<template>
<!-- 将模态框渲染到 body 下 -->
<Teleport to="body">
<div class="modal">模态框内容</div>
</Teleport>
</template>
Suspense(异步组件):
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
6. 生命周期变化
| Vue2 | Vue3 Options API | Vue3 Composition API |
|---|---|---|
| beforeCreate | beforeCreate | setup() |
| created | created | setup() |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | beforeUnmount | onBeforeUnmount |
| destroyed | unmounted | onUnmounted |
7. 移除的特性
Vue3 移除了:
$on、$off、$once(事件总线)filter(过滤器,推荐用计算属性或方法替代)$children(推荐用 ref 访问子组件)inline-template$destroy
8. v-model 变化
Vue2:
- 只能绑定一个 v-model
- 默认绑定 value 属性和 input 事件
Vue3:
- 支持多个 v-model
- 默认绑定 modelValue 属性和 update:modelValue 事件
<!-- Vue3 多个 v-model -->
<template>
<UserForm
v-model:name="userName"
v-model:age="userAge"
/>
</template>
核心优势总结
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy |
| 代码组织 | Options API | Composition API |
| 性能 | 一般 | 更快(编译优化、Tree-shaking) |
| 体积 | 较大 | 减少 40% |
| TypeScript | 支持一般 | 完善支持 |
| 多根节点 | ❌ 不支持 | ✅ 支持 |
| Teleport | ❌ 无 | ✅ 有 |
| Suspense | ❌ 无 | ✅ 有 |
核心要点:
Vue3 在响应式机制、性能、TypeScript 支持、代码组织等方面全面升级,推荐新项目使用 Vue3,老项目可根据需求逐步迁移。
5.2 Vue3 常用 Composition API
难度:⭐⭐⭐⭐⭐
问题描述
Vue3 Composition API 有哪些常用的 API?如何使用?
答案解析
1. 响应式 API
ref - 创建基本类型响应式数据
<script setup>
import { ref } from 'vue'
// 创建响应式数据
const count = ref(0)
const message = ref('Hello')
// 访问值(必须通过 .value)
console.log(count.value) // 0
// 修改值
count.value++
message.value = 'Hi'
// 在模板中自动解包,无需 .value
</script>
<template>
<div>{{ count }}</div> <!-- 自动解包 -->
<div>{{ message }}</div>
</template>
reactive - 创建对象/数组响应式数据
<script setup>
import { reactive } from 'vue'
// 创建响应式对象
const user = reactive({
name: '张三',
age: 20,
hobbies: ['读书', '运动']
})
// 直接访问和修改(无需 .value)
console.log(user.name) // '张三'
user.age = 21
user.hobbies.push('旅游')
</script>
<template>
<div>{{ user.name }} - {{ user.age }}</div>
</template>
toRef / toRefs - 保持响应式关联
<script setup>
import { reactive, toRef, toRefs } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
// toRef:转换单个属性
const ageRef = toRef(user, 'age')
ageRef.value = 21 // 会同步更新 user.age
// toRefs:转换所有属性(常用于解构)
const { name, age } = toRefs(user)
name.value = '李四' // 会同步更新 user.name
</script>
isRef / isReactive - 类型判断
import { ref, reactive, isRef, isReactive } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
console.log(isRef(count)) // true
console.log(isReactive(user)) // true
2. 计算属性与监听
computed - 计算属性
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const double = computed(() => count.value * 2)
// 可写计算属性
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get: () => `${firstName.value}${lastName.value}`,
set: (val) => {
const names = val.split('')
firstName.value = names[0]
lastName.value = names[1]
}
})
fullName.value = '李四' // 触发 setter
</script>
watch - 监听数据变化
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 20 })
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 监听 reactive 的某个属性
watch(() => user.age, (newVal, oldVal) => {
console.log(`age 从 ${oldVal} 变为 ${newVal}`)
})
// 监听多个数据
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${newCount}, age: ${newAge}`)
})
// 深度监听
watch(user, (newVal) => {
console.log('user 发生变化', newVal)
}, { deep: true })
// 立即执行
watch(count, (newVal) => {
console.log('count:', newVal)
}, { immediate: true })
</script>
watchEffect - 自动追踪依赖
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 自动追踪 count 和 message 的变化
watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
})
// 停止监听
const stop = watchEffect(() => {
console.log(count.value)
})
// 手动停止
stop()
</script>
3. 生命周期钩子
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 挂载前
onBeforeMount(() => {
console.log('组件即将挂载')
})
// 挂载后
onMounted(() => {
console.log('组件已挂载')
// 适合:操作 DOM、发起请求、添加事件监听
})
// 更新前
onBeforeUpdate(() => {
console.log('组件即将更新')
})
// 更新后
onUpdated(() => {
console.log('组件已更新')
})
// 卸载前
onBeforeUnmount(() => {
console.log('组件即将卸载')
// 适合:清除定时器、移除事件监听
})
// 卸载后
onUnmounted(() => {
console.log('组件已卸载')
})
</script>
4. 依赖注入
provide / inject - 跨层级传递数据
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// 提供数据
provide('theme', theme)
provide('updateTheme', updateTheme)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 接收数据(第二个参数为默认值)
const theme = inject('theme', 'light')
const updateTheme = inject('updateTheme')
// 使用
updateTheme('light')
</script>
5. 其他常用 API
getCurrentInstance - 获取组件实例
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
// 访问 $refs、$emit 等
console.log(instance.refs)
console.log(instance.emit)
</script>
nextTick - 等待 DOM 更新
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const updateCount = async () => {
count.value++
// 等待 DOM 更新完成
await nextTick()
// 此时 DOM 已更新
console.log('DOM 已更新')
}
</script>
defineProps / defineEmits - 组件通信
<script setup>
import { defineProps, defineEmits } from 'vue'
// 定义 props
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
}
})
// 定义 emits
const emit = defineEmits(['update', 'delete'])
// 触发事件
const handleUpdate = () => {
emit('update', props.count + 1)
}
</script>
defineExpose - 暴露给父组件
<script setup>
import { ref, defineExpose } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 暴露给父组件
defineExpose({
count,
increment
})
</script>
常用 API 总结
| 分类 | API | 用途 |
|---|---|---|
| 响应式 | ref | 基本类型响应式 |
| reactive | 对象/数组响应式 | |
| toRef / toRefs | 保持响应式关联 | |
| 计算与监听 | computed | 计算属性 |
| watch | 监听数据变化 | |
| watchEffect | 自动追踪依赖 | |
| 生命周期 | onMounted | 挂载后 |
| onBeforeUnmount | 卸载前 | |
| 依赖注入 | provide / inject | 跨层级传递数据 |
| 组件通信 | defineProps | 接收 props |
| defineEmits | 触发事件 | |
| defineExpose | 暴露给父组件 |
核心要点:
Composition API 提供了更灵活的代码组织方式,核心是 ref/reactive 创建响应式数据,computed/watch 处理逻辑,生命周期钩子管理副作用。
六、路由相关
6.1 Hash 模式和 History 模式的区别
难度:⭐⭐⭐
问题描述
Vue Router 的 Hash 模式和 History 模式有什么区别?各自的优缺点是什么?
答案解析
一、URL 形态
Hash 模式:
- URL 中包含
#符号 #后的内容为前端路由,不会发送到服务器
http://example.com/#/home
http://example.com/#/user/123
http://example.com/#/about
History 模式:
- URL 无
#,形态更接近传统 URL - 更美观,更符合用户习惯
http://example.com/home
http://example.com/user/123
http://example.com/about
二、实现原理
Hash 模式:
- 基于
location.hash和hashchange事件 - 监听 URL 中
#后的变化更新页面内容 #后的内容不会发送到服务器
// Hash 模式原理
window.addEventListener('hashchange', () => {
const hash = location.hash.slice(1) // 去掉 #
console.log('路由变化:', hash)
// 根据 hash 渲染对应组件
})
// 改变路由
location.hash = '/home'
History 模式:
- 基于 HTML5 History API(
pushState、replaceState) - 通过
popstate事件监听路由变化 - 修改浏览器历史记录栈,不刷新页面
// History 模式原理
window.addEventListener('popstate', (event) => {
console.log('路由变化:', location.pathname)
// 根据 pathname 渲染对应组件
})
// 改变路由(不刷新页面)
history.pushState({}, '', '/home')
history.replaceState({}, '', '/about')
三、服务端配置
Hash 模式:
- ✅ 无需服务端配置
- 直接部署即可正常使用
#后的内容不会发送到服务器,服务器始终返回 index.html
History 模式:
- ❌ 需要服务端支持
- 直接访问子路由会返回 404 错误
- 需要配置服务器,将所有路由重定向到 index.html
Nginx 配置示例:
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
Apache 配置示例:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Node.js (Express) 配置示例:
const express = require('express')
const path = require('path')
const app = express()
app.use(express.static(path.join(__dirname, 'dist')))
// 所有路由都返回 index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})
app.listen(3000)
四、浏览器兼容性
Hash 模式:
- ✅ 兼容所有浏览器(包括 IE8+)
- 基于 URL hash,所有浏览器都支持
History 模式:
- ❌ 依赖 HTML5 History API
- 仅支持 IE10+ 及现代浏览器
- 不支持 IE9 及以下版本
五、SEO 优化
Hash 模式:
- ❌ 对 SEO 不友好
#后的内容可能被搜索引擎忽略- 搜索引擎可能将所有页面视为同一个页面
History 模式:
- ✅ 对 SEO 更友好
- 完整 URL 更易被搜索引擎抓取
- 但仍需配合服务端渲染(SSR)实现完整 SEO 优化
六、Vue Router 配置
Hash 模式(默认):
// Vue Router 3 (Vue2)
import VueRouter from 'vue-router'
const router = new VueRouter({
mode: 'hash', // 默认值
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
})
// Vue Router 4 (Vue3)
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
})
History 模式:
// Vue Router 3 (Vue2)
import VueRouter from 'vue-router'
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
})
// Vue Router 4 (Vue3)
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
})
七、适用场景
选择 Hash 模式:
- ✅ 对 SEO 无要求的应用
- ✅ 需要兼容老旧浏览器
- ✅ 无法配置服务器的场景
- ✅ 快速开发和部署
- ✅ 内部管理系统、后台系统
选择 History 模式:
- ✅ 追求 URL 美观性
- ✅ 需要 SEO 优化的项目
- ✅ 具备服务端配置能力
- ✅ 面向用户的网站
- ✅ 现代浏览器环境
八、对比总结
| 对比项 | Hash 模式 | History 模式 |
|---|---|---|
| URL 形态 | #/home | /home |
| 实现原理 | hashchange 事件 | History API |
| 服务端配置 | ✅ 无需配置 | ❌ 需要配置 |
| 浏览器兼容 | ✅ IE8+ | ❌ IE10+ |
| SEO 优化 | ❌ 不友好 | ✅ 更友好 |
| URL 美观 | ❌ 有 # 符号 | ✅ 无 # 符号 |
| 部署难度 | ✅ 简单 | ❌ 需要服务器支持 |
| 适用场景 | 内部系统、快速开发 | 面向用户的网站 |
九、常见问题
1. History 模式刷新 404 问题
问题: 直接访问 http://example.com/user/123 返回 404
原因: 服务器找不到 /user/123 对应的文件
解决: 配置服务器,将所有路由重定向到 index.html
2. Hash 模式锚点冲突
问题: 页面内锚点跳转与路由冲突
解决: 使用 scrollBehavior 配置
const router = new VueRouter({
mode: 'hash',
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
return { selector: to.hash }
}
}
})
核心要点:
Hash 模式简化了部署和维护,但牺牲了 URL 规范性和 SEO;History 模式提供了更专业的用户体验,但需额外服务端支持。开发者应根据项目需求权衡选择。
总结
本文档系统整理了 Vue.js 的核心面试题,涵盖了从基础概念到高级特性的各个方面:
- 基础概念:构建工具、MVVM/MVC、指令、data 函数、ref 使用
- 组件通信:Props/Emit、Vuex、Pinia、Provide/Inject
- 生命周期:Vue2/3 生命周期对比、使用场景
- 响应式原理:Object.defineProperty vs Proxy
- Vue3 新特性:Composition API、性能优化、新增特性
- 路由相关:Hash 模式 vs History 模式
掌握这些知识点,能够帮助你在 Vue.js 面试中脱颖而出,也能在实际项目开发中游刃有余。
学习建议:
- 理解原理比记忆 API 更重要
- 多动手实践,加深理解
- 关注 Vue 官方文档的更新
- 结合实际项目场景学习
祝你面试顺利!🎉
更多推荐


所有评论(0)