1. Vue 生命周期详解 (Vue 2 vs Vue 3)
| 生命周期阶段 | Vue 2 | Vue 3 Composition API | 说明 |
|---|---|---|---|
| 创建前 | beforeCreate | ❌ 被 setup 替代 | 组件实例初始化 |
| 创建完成 | created | ❌ 被 setup 替代 | 组件数据方法初始化( data、methods、computed 等) |
| 挂载前 | beforeMount | onBeforeMount | 组件尚未挂载到 DOM , template 已解析 |
| 挂载后 | mounted | onMounted | 组件挂载到 DOM ,可进行 DOM 操作、接口请求 |
| 更新前 | beforeUpdate | onBeforeUpdate | 数据更新,DOM 未更新 |
| 更新后 | updated | onUpdated | 数据更新,DOM 更新 |
| 销毁前 | beforeDestroy | onBeforeUnmount | 组件实例未销毁,做清理工作(如定时器、事件) |
| 销毁后 | destroyed | onUnmounted | 组件实例已销毁,所有绑定事件、子组件也被销毁 |
| 缓存激活 | — | onActivated | 组件被缓存后(KeepAlive)恢复显示,激活恢复逻辑 |
| 缓存失活 | — | onDeactivated | 组件被缓存后(KeepAlive)停用但未销毁,暂停或清理逻辑 |
父子组件生命周期执行顺序:
- 加载:
- 父:beforeCreate → created → beforeMount
- 子:beforeCreate → created → beforeMount → mounted
- 父:mounted
- 销毁:()
- 父:beforeDestroy
- 子:beforeDestroy → destroyed
- 父:destroyed
数据请求时机选择:
| 生命周期钩子 | 触发时机 | 是否可访问 DOM | 特点 |
|---|---|---|---|
created | 更早 | ❌ 组件未挂载到 DOM | 适合纯数据加载,无 DOM 可操作 |
mounted | 更晚 | ✅ 组件已挂载到 DOM | 适合对 DOM 进行操作,或依赖 DOM 的请求 |
2. Vue2 和 Vue3 响应式原理
| 对比点 | Vue2 | Vue3 |
|---|---|---|
| 核心 API | Object.defineProperty() | Proxy + Reflect |
| 实现方式 | 劫持属性,递归遍历对象的每个属性 | 代理对象,拦截所有属性的读写操作 |
| 是否支持新增/删除属性 | ❌ 需要 Vue.set() | ✅ 自动响应 |
| 数组响应式 | ❌ API 操作不支持 | ✅ 原生支持 |
| 性能 | 较差,需递归监听 | 更优,按需代理 |
| 响应式深度 | 需要递归,嵌套层级越深性能越差 | 延迟代理,性能更好 |
Vue 2(基于 Object.defineProperty 劫持属性)
实现方式:
- Vue2 通过 递归遍历对象的每个属性,用
Object.defineProperty()为对象添加属性 getter/setter。 - 读取属性时触发
getter,收集依赖。 - 修改属性时触发
setter,触发更新。
特点:
- 只能劫持已存在的属性,新增属性无响应性。
- 数组方法不能被劫持。
简化示意代码:
Object.defineProperty(data, 'name', {
get() {
// 收集依赖
return val;
},
set(newVal) {
// 通知更新
val = newVal;
}
});
对象响应式更新
this.obj = { a: 1 }
this.obj.b = 2 // ❌ Vue 不会追踪 b,视图不会更新
-
使用
Vue.set或this.$setVue 会在内部将属性用 Object.defineProperty 添加到对象,并触发视图更新。
this.$set(this.obj, 'b', 2) // ✅ 响应式 // 或者 Vue.set(this.obj, 'b', 2) // ✅ 响应式 -
使用
Object.assign或{...}替换整个数组Object.assign(this.obj, { b: 2 }) // ❌ 视图不更新,只是给现有对象加属性 this.obj = Object.assign({}, this.obj, { b: 2 }) // ✅ 响应式,用新对象替换旧对象,视图就会更新 this.obj = { ...this.obj, b: 2 } // ✅ 响应式,用新对象替换旧对象,视图就会更新 -
使用
this.$forceUpdate()强制更新组件强制触发组件重新渲染,通常用于非响应式数据变更时,临时手动刷新视图。
this.obj.b = 2 // ❌ 无响应 this.$forceUpdate() // ✅ 强制视图刷新
数组响应式更新
this.arr = [0]
this.arr[0] = 'new' // ✅ 响应式
this.arr[1] = 'new' // ❌ 不响应式
this.arr.length = 0 // ❌ 不响应式
-
使用
Vue.set或this.$setVue.set(this.arr, 1, 'new') // ✅ 响应式 this.$set(this.arr, 1, 'new') // ✅ 响应式 -
使用
[...]替换整个数组this.arr = [...this.arr, 'new'] // ✅ 响应式(替换引用) -
使用变异方法(Vue 已拦截过这些方法)
方法 描述 push添加新元素 pop移除末尾元素 shift移除头部元素 unshift添加头部元素 splice增删改元素 sort排序 reverse反转 this.arr.push('new') // ✅ 响应式 this.arr.splice(1, 1, 'new') // ✅ 响应式 this.arr.splice(0) // ✅ 响应式清空
Vue 3(基于 Proxy 代理整个对象)
实现方式:
- Vue3 使用
Proxy代理整个对象,一次性拦截所有属性的读写操作。 - 读取属性时触发
get,收集依赖。 - 修改属性时触发
set,触发更新。
特点:
- 可劫持对象新增属性,支持深层响应式。
- 完整拦截各种操作,包括数组方法。
简化示意代码:
const proxy = new Proxy(data, {
get(target, key) {
// 收集依赖
return Reflect.get(target, key);
},
set(target, key, value) {
// 通知更新
return Reflect.set(target, key, value);
}
});
3. Vue 双向数据绑定 和 MVVM 开发模式
双向数据绑定:
- 数据变化后驱动更新视图,视图变化后自动更新数据。
框架(MVVM 开发模式)构成:
- 数据层(Model):数据处理与业务逻辑,例如 Ajax 请求、数据持久化等;
- 视图层(View):用户界面,负责将数据呈现给用户;
- 业务逻辑层(ViewModel):连接 Model 和 View,监听数据变化后更新视图,监听视图变化后更新数据。
Vue 双向数据绑定原理:
-
通过
v-model实现的,基于 数据劫持 和 事件监听。-
数据劫持:劫持数据,监听数据变化,触发视图更新。
- Vue 2:
Object.defineProperty - Vue 3:
Proxy+Reflect
- Vue 2:
-
事件监听:监听 DOM 事件,捕获输入变化,触发数据更新。
- 监听
input/change等 DOM 事件,更新数据模型
- 监听
-
v-model 实现:v-bind绑定值,再通过v-on:input来修改值
<input v-model="msg" />
<!-- 等价于 -->
<input :value="msg" @input="msg = $event.target.value" />
4. Vue 创建响应式数据核心 API ref() 和 reactive()
ref()
用于创建基本类型或单个值的响应式引用,也可用于包裹对象。
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 访问时需要 .value
reactive()
用于创建引用类型(如对象、数组、Map)的响应式代理。
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Vue'
})
console.log(state.count) // 直接访问,无需 .value
// 解构
const { count, name } = toRefs(state);
console.log(count.value) // 访问时需要 .value
主要区别
| 特性 | ref() | reactive() |
|---|---|---|
| 适用类型 | 基本类型 | 引用类型(对象、数组、Map 等) |
| 内部原理 | Object.defineProperty劫持数据属性 | Proxy 代理对象 Reflect 操作属性 |
| 使用语法 | .value 来访问或修改 | 直接访问属性 |
| 解构是否丢响应性 | 会丢失(除非用 toRefs/toRef) | 会丢失(除非用 toRefs/toRef) |
| 是否可用于 DOM 引用 | 是 | 否 |
5. computed 与 watch 的区别
| 对比项 | computed | watch |
|---|---|---|
| 主要用途 | 计算属性,依赖其他数据 | 监听属性变化,执行副作用逻辑 |
| 是否缓存 | ✅ 有缓存,依赖不变不重新计算 | ❌ 无缓存,每次变化都执行 |
| 返回值 | ✅ 返回计算结果 | ❌ 无返回值 |
| 支持异步操作 | ❌ 不适合异步 | ✅ 支持异步 |
| 是否能监听多个值 | ❌ 一般用于一个表达式 | ✅ 可以监听多个响应式属性(数组或函数形式) |
| 适合使用场景 | 用于响应式属性的计算 | 用于执行异步操作、监听多个数据变化等 |
6. v-if 和 v-show 的区别
| 特性 | v-if | v-show |
|---|---|---|
| 原理 | 动态添加或移除 DOM 元素 | 切换 DOM 元素的 display: none 样式 |
| DOM 状态 | 每次切换都会销毁和重建 DOM | DOM 元素始终存在,只是显示隐藏 |
| 切换频率 | 适合不频繁切换的场景 | 适合频繁切换的场景 |
7. v-if 和 v-for 的优先级问题
v-if:条件性渲染 DOM 元素。
v-for:基于数组循环渲染一个列表。
Vue 2: v-for 优先于 v-if
- 循环渲染元素,最后判断每一项是否显示。
<li v-for="item in items" v-if="item.visible">
{{ item.text }}
</li>
-
注意事项:
- 避免在同一元素上同时使用
v-if和v-for。 - 避免对每一项进行条件渲染,对整个列表统一控制。把
v-if至于v-for外层。
- 避免在同一元素上同时使用
-
推荐做法: 预处理数据,用计算属性提前过滤数据。
<ul>
<li v-for="item in visibleItems" :key="item.id">
{{ item.text }}
</li>
</ul>
<script>
export default {
computed: {
visibleItems() {
return this.items.filter(item => item.visible);
}
}
}
</script>
Vue 3: v-if 优先于 v-for
- 判断每一项是否显示,再循环渲染元素。
- v-if 条件无法访问 v-for 作用域内定义的变量别名,无法正常显示。
- 推荐做法: v-for 置于容器元素,v-if 置于内部元素。
<ul v-for="item in items">
<li v-if="item.visible">{{ item.text }}</li>
</ul>
8. Vue 修饰符(.xxx)
Vue 的修饰符(Modifiers)通过简洁的语法,将常见的 DOM 操作和数据处理逻辑封装成可复用的模式,用于简化或增强指令的行为。
表单修饰符(Form Modifiers):用于 v-model 指令,优化表单输入处理。
-
.lazy:将输入事件改为change事件(输入完成后更新数据)。<input v-model.lazy="message"> <!-- 输入框失焦后更新数据 --> -
.number:将输入值转为数字类型。<input v-model.number="age" type="number"> <!-- 自动转为 Number --> -
.trim:自动去除输入内容的首尾空格。<input v-model.trim="username"> <!-- 输入 " Vue " 转为 "Vue" -->
事件修饰符:用于 v-on 指令,优化事件处理逻辑。
-
.stop:阻止事件冒泡(等价于event.stopPropagation())。<button @click.stop="handleClick">点击不会冒泡</button> -
.capture: 使用事件捕获模式(从外到内触发)。<div @click.capture="handleCapture">捕获阶段触发</div> -
.prevent: 阻止默认行为(等价于event.preventDefault())。<form @submit.prevent="handleSubmit">表单不会刷新页面</form> -
.self:仅当事件在元素本身(而非子元素)触发时执行。<div @click.self="handleSelf">点击子元素不会触发</div> -
.once:事件只触发一次。<button @click.once="handleOnce">仅第一次点击有效</button> -
修饰符链式调用 :可以组合使用多个修饰符,顺序可能影响结果。
<button @click.stop.prevent="handleClick">阻止冒泡和默认行为</button>
按键修饰符(Key Modifiers):用于监听特定键盘事件。
-
直接使用按键名 :
.enter,.esc,.space等<input @keyup.enter="submit"> <!-- 按下回车触发 --> <input @keyup.esc="cancel"> <!-- 按下 ESC 触发 --> <input @keyup.space="jump"> <!-- 按下空格触发 --> -
系统修饰键
<input @keyup.ctrl.s="save"> <!-- Ctrl + S 触发 --> <input @keyup.shift.enter="submit"> <!-- Shift + Enter 触发 --> -
.exact修饰符:精确控制组合键。<button @keyup.ctrl.exact="ctrlOnly">仅按下 Ctrl 时触发</button> -
监听指定鼠标键 :
.left,.right,.middle<button @click.left="handleLeftClick">左键触发</button> <!-- 左键点击 --> <!-- 右键点击(默认会弹出浏览器菜单,需配合 .prevent 阻止) --> <button @click.right.prevent="handleRightClick">右键触发</button> <button @click.middle="handleMiddleClick">中键触发</button> <!-- 中键点击 -->
其他修饰符
-
.sync修饰符(Vue 2) :实现父子组件的双向绑定(Vue 3 中已被v-model替代)。<!-- Vue 2 父组件 --> <Child :title.sync="pageTitle" /> <!-- 子组件触发 --> props: { title: { type: String, default: '' }, } this.$emit('update:title', newTitle); <!-- Vue 3 父组件 --> <Child v-model:title.sync="pageTitle" /> <!-- 子组件触发 --> const props = defineProps(['title']); const emit = defineEmits(['update:title']); emit('update:title', newTitle); -
.native修饰符(Vue 2) :监听组件根元素的原生事件(Vue 3 已移除)。<!-- Vue 2 --> <MyComponent @click.native="handleClick" />
9. Vue 自定义指令(v-xxx)
在 Vue 中,自定义指令(Custom Directives)允许开发者封装对 DOM 元素的直接操作,用于扩展 Vue 的模板功能。适合处理底层 DOM 交互(如聚焦输入框、绑定事件、集成第三方库等)。
指令的创建与注册
-
全局注册: Vue3 在main.js中通过
app.directive方法注册// main.js import { createApp } from 'vue'; const app = createApp(App); // 注册全局指令 'v-focus' app.directive('focus', { mounted(el) { el.focus(); } }); app.mount('#app'); -
局部注册:在组件中通过
directives选项注册export default { directives: { focus: { mounted(el) { el.focus(); } } } } -
使用:
<input v-focus/>
指令的参数与上下文
el:指令绑定的 DOM 元素。binding:包含指令信息的对象:name:指令名,不包括 v- 前缀。value:指令的绑定值(如v-my-directive="value")。oldValue:旧值(仅在beforeUpdate和updated中可用)。arg:指令的参数(如v-my-directive:arg)。modifiers:修饰符对象(如v-my-directive.modifier→{ modifier: true })。instance:使用指令的组件实例。
vnode:虚拟节点。prevNode:上一个虚拟节点(仅在beforeUpdate和updated中可用)。
高级用法
-
绑定多个值:通过对象传递多个参数
<div v-demo="{ color: 'red', text: 'Hello' }"></div>app.directive('demo', { mounted(el, binding) { console.log(binding.value.color); // 'red' console.log(binding.value.text); // 'Hello' } }); -
使用修饰符:检测修饰符并执行不同逻辑
<button v-click.once="handleClick">只能点击一次</button>app.directive('click', { mounted(el, binding) { let handler = binding.value; if (binding.modifiers.once) { let called = false; handler = () => { if (!called) { binding.value(); called = true; } }; } el.addEventListener('click', handler); } });
使用示例
-
自动聚焦输入框:
app.directive('focus', { mounted(el) { el.focus(); } });<input v-focus /> -
动态修改元素颜色:
app.directive('color', { mounted(el, binding) { el.style.color = binding.value; }, updated(el, binding) { el.style.color = binding.value; } });<p v-color="'red'">这段文字会显示为红色</p> -
权限控制指令:
app.directive('permission', { mounted(el, binding) { const userPermissions = ['edit', 'delete']; if (!userPermissions.includes(binding.value)) { el.style.display = 'none'; } } });<button v-permission="'edit'">编辑</button>
10. Vue 组件通信方式(整理对比版)
| 通信方式 | 适用场景与说明 |
|---|---|
| Props / Emit | 父向子传值用 props,子向父用 $emit 触发自定义事件 |
| ref / $refs | 父组件通过 $refs 获取子组件实例,适合访问方法或属性 |
| EventBus | 使用中央事件总线在兄弟组件或任意组件之间通信($bus.$emit触发,$bus.$on监听) |
| Vuex | 全局状态管理,适用于复杂项目跨组件共享数据 |
| Pinia(Vue3推荐) | 替代 Vuex 的新一代状态管理库,更轻量、更简单 |
| provide / inject | provide在祖先组件中提供数据,inject在后代组件中注入数据,适用于组件库开发、依赖注入等场景 |
| attrs / listeners | 子组件中通过 $attrs 接收父组件未显式声明的属性,通过 $listeners 接收事件(Vue3 中整合到了 $attrs) |
| 插槽(slots) | 允许父组件向子组件插入模板内容,支持作用域插槽反向传递数据 |
Vue 的单向数据流
组件之间的数据流动,始终从 父组件 → 子组件 单向流动,子组件通过 props 接收数据(v-bind),不能直接修改,通过 $emit 通知父组件更新(v-on)。
好处:数据来源清晰,易于追踪;逻辑清晰,便于维护与调试;模块化,降低耦合;
特殊情况:传递数据的类型为引用类型,会存在副作用,即直接在子组件修改对象的属性,父组件会响应式更新。本质仍然是单向数据流,因为 props 的引用未变,仍然指向同一地址。
11. Vue3 中 setup 下的父子通信写法
| 方向 | 方法 | 子组件写法 | 父组件写法 |
|---|---|---|---|
| 父 → 子 | Props | defineProps | :propName="value" |
| 子 → 父 | Emit | defineEmits | @event-name="handler" |
| 子暴露给父 | Expose | defineExpose | 通过 ref 调用 |
父子组件通信(defineProps + defineEmits)
- 父组件:v-bind传递数据,v-on监听事件。
<template>
<ChildComponent :message="parentMessage" @update-message="handleUpdate" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from parent!');
const handleUpdate = (newMessage) => {
parentMessage.value = newMessage;
};
</script>
- 子组件:defineProps接收数据,defineEmits触发事件。
<script setup>
// 使用 defineProps 声明 props(无需导入)
const props = defineProps({
message: {
type: String,
required: true
}
});
// 直接使用 props.message
console.log(props.message);
// 使用 defineEmits 声明 emits(返回一个 emit 函数)
const emit = defineEmits(['updateMessage']);
const handleClick = () => {
emit('updateMessage', 'New message from child!');
};
</script>
- v-model简化:
// 父组件
<template>
<!-- 使用 v-model 双向绑定 -->
<ChildComponent v-model:message="parentMessage" />
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from parent!');
</script>
// 子组件
<script setup>
// 使用 v-model 约定的 prop 名和事件
const props = defineProps({
message: {
type: String,
required: true
}
});
const emit = defineEmits(['update:message']);
const handleClick = () => {
// 触发 v-model 约定的事件名
emit('update:message', 'New message from child!');
};
</script>
子组件暴露方法或属性(defineExpose)
- 子组件:暴露内容
<script setup>
const someMethod = () => {
console.log('Child method called!');
};
// 使用 defineExpose 暴露方法或属性
defineExpose({ someMethod });
</script>
- 父组件:通过 ref 调用
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script setup>
import { ref } from 'vue';
const childRef = ref(null);
const callChildMethod = () => {
childRef.value.someMethod(); // 输出:'Child method called!'
};
</script>
12. setup 函数
什么是 setup() 函数?
setup() 是 Vue 3 Composition API 的入口函数,在组件创建阶段执行,用于定义响应式数据、方法、生命周期等逻辑。
setup() 接收 props 和 context 参数,返回对象(响应式数据、方法),暴露给模板使用。
相比 Vue 2 的 Options API,setup 更灵活、更方便逻辑复用,但不再使用 this。
作用
替代 Vue 2 中的 data, methods, computed, watch 等选项
- 初始化 响应式数据(
ref、reactive) - 编写 业务逻辑函数(事件处理等)
- 使用 生命周期函数(如
onMounted()) - 注册 计算属性(
computed) - 使用 依赖注入(inject) 和 props
参数
setup(props, context) {
// props: 父组件传递的响应式数据(解构会丢失响应式,使用 toRefs(props))
// context: 上下文对象,包含:
// - attrs: 非 props 的 attribute
// - slots: 插槽内容
// - emit: 事件派发函数
// - expose: 暴露组件方法
}
setup(props, { attrs, slots, emit, expose }) { ... }
返回值
- 返回一个对象,对象中的属性或方法将暴露给组件的模板(template)
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment } // 模板可直接使用
}
script setup 简化语法
Vue 3 提供 <script setup>,可以自动处理 setup 逻辑,写法更简洁:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
优点:
- 更少的模板代码(不用手动 return)
- 更好地支持类型推导(TS)
- 更清晰的逻辑组织结构
13. Vuex Store 和 Pinia
全局状态管理器,集中存储跨组件共享数据。
Vuex Store
- State:存储共享变量,定义默认值(
this.$store.state.moduleName.xxx) - Getter:派生计算属性,获取变量计算值(
this.$store.getters['moduleName/getter名']) - Mutation:修改变量值(
this.$store.commit('moduleName/mutation名')) - Action:支持异步操作,最终提交 Mutation(
this.$store.dispatch('moduleName/action名', payload)) - Module:模块化管理多个store,提高可维护性
Vuex Store vs Pinia
| 项目 | Vuex | Pinia |
|---|---|---|
| 支持版本 | Vue 2 / Vue 3 | Vue 3(支持 Vue 2 需插件) |
| API 风格 | Options API 风格 | Composition API 风格 |
| 核心概念 | state / getters / mutations / actions / modules | state / getters / actions |
| 状态修改方式 | 必须通过 mutations 同步修改 | 直接修改 state 或用 actions(更自然) |
| 模块组织 | 手动命名空间 + modules | 更自由,支持组合式模块化 |
| 代码体积 | 大(~20KB) | 小(~1.5KB) |
14. Vue Router 创建路由方式
- 安装
vue-router
npm install vue-router@4 # Vue3 对应 Vue Router 4.x
- 创建
router/index.js定义路由配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/', // 路径
name: 'Home', // 路由名称(可选)
component: () => import('@/views/Home.vue') // 组件(推荐懒加载)
},
{
path: '/user/:id', // 动态路由(通过 params 传参)
component: () => import('@/views/User.vue'),
props: true // 将 params 作为 props 传递
},
{
path: '/about',
component: () => import('@/views/About.vue'),
query: { // 默认 query 参数(可选)
lang: 'en'
}
}
];
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 历史模式
routes
});
export default router;
- 在
main.js中注册路由:
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
- 使用
<router-view>、<router-link>组件,实现路由视图与跳转。
15.route 和 router 的区别
| 特性 | route(当前路由对象) | router(路由实例) |
|---|---|---|
| 作用 | 提供当前路由的信息(路径、参数、查询等) | 用于编程式导航(跳转、前进、后退等) |
| 常用属性 | path, params, query, hash, fullPath | push, replace, go, back, forward |
| 示例 | route.params.id | router.push('/user/123') |
16. params 与 query 的区别
| 特性 | params(路径参数) | query(查询参数) |
|---|---|---|
| 使用场景 | 动态路径参数 | 查询参数(如分页、筛选) |
| 表现形式 | /user/123 | /user?id=123 |
| 路由配置 | 需在路由配置中使用 : 声明参数:path: '/user/:id' | 无需在路由路径中定义 |
| 刷新保留 | 需在路由配置 props: true | 刷新后参数依然保留 |
| 获取方式(Vue2) | this.$route.params.id | this.$route.query.id |
| 获取方式(Vue3) | useRoute().params.id | useRoute().query.id |
适用场景
params:- 用于标识资源的唯一性(如用户 ID
/user/123)。 - 需要提前在路由配置中定义参数(
path: '/user/:id')。
- 用于标识资源的唯一性(如用户 ID
query:- 用于可选的过滤条件或分页(如
/products?category=books&page=2)。 - 无需在路由配置中声明参数。
- 用于可选的过滤条件或分页(如
17. 路由守卫机制
Vue Router 的路由守卫(Navigation Guards) 用于在路由跳转过程中进行权限控制、数据预加载或取消导航等操作。
路由守卫类型
| 守卫类型 | 执行时机 | 使用场景 | 方法 |
|---|---|---|---|
| 全局前置守卫 | 路由跳转前触发(最早执行) | 全局登录验证 | beforeEach |
| 路由独享守卫 | 进入特定路由前触发 | 路由权限控制 | beforeEnter |
| 组件内守卫 | 组件渲染前/更新时/离开前触发 | 数据预加载或数据保存提示 | beforeRouteEnter、beforeRouteLeave |
| 全局解析守卫 | 所有组件守卫和异步组件解析后触发 | 确保异步数据加载完成 | beforeResolve |
| 全局后置钩子 | 导航完成后触发(无修改导航能力) | 页面访问统计、日志记录 | afterEach |
执行顺序
全局 beforeEach → 路由独享 beforeEnter → 组件 beforeRouteEnter → 全局 beforeResolve → 全局 afterEach
关键参数说明
| 参数 | 作用 |
|---|---|
to | 目标路由对象(包含 path, params, query, meta 等) |
from | 正要离开的路由对象 |
next | 控制导航行为的函数: - next():继续导航- next(false):终止导航- next('/path'):重定向 |
18. Hash 与 History 模式的区别
Vue Router 支持 Hash 和 History 两种模式。
| 对比项 | Hash 模式(默认模式) | History 模式 |
|---|---|---|
| 描述 | URL 中 # 后拼接路由路径 | URL 中直接拼接路由路径 |
| URL 形式 | /#/page | /page |
| 浏览器支持 | 所有浏览器 | IE10+ |
| 是否美观 | 否(含 #) | 是 |
| SEO 友好性 | 差,模拟路径爬虫难抓取 | 好,真实路径 URL 结构清晰 |
| 是否需要服务端配置 | 否 | 是(否则刷新 404) |
| 原理 | hashchange 事件 | HTML5 History API |
Hash 模式
-
特点:
- URL 形如:
https://example.com/#/home #后的内容不会被浏览器发送给服务器,是模拟路径- 利用浏览器原生的
hashchange事件监听地址变化
- URL 形如:
-
✅ 优点:
- 兼容性强,支持所有浏览器
- 不需要服务端配合,直接部署即可访问
-
❌ 缺点:
- URL 带有
#,不美观 - SEO 不友好,搜索引擎爬虫识别较差
- URL 带有
History 模式
-
特点:
- URL 形如:
https://example.com/home - 使用 HTML5 History API(如
pushState()和popState())
- URL 形如:
-
✅ 优点:
- 路径更简洁美观
- 真实路径,SEO 友好,便于搜索引擎抓取
-
❌ 缺点:
- 需要服务器配置支持:刷新页面时服务器要将所有路径重定向到
index.html - 如果未配置正确,刷新会出现
404 Not Found
- 需要服务器配置支持:刷新页面时服务器要将所有路径重定向到
使用方式(以 Vue Router 为例)
// Hash 模式(默认)
const router = new VueRouter({
mode: 'hash',
routes: [...]
})
// History 模式
const router = new VueRouter({
mode: 'history',
routes: [...]
})
19. 虚拟 DOM(Virtual DOM)
-
虚拟 DOM:模拟真实 DOM 树结构的 JavaScript 对象,实现页面的高效渲染与更新。
-
实现原理:
- 创建虚拟节点(VNode):将 DOM 元素编译为虚拟节点对象。
- 创建虚拟 DOM:创建由多个 VNode 组成的虚拟 DOM 树。
- Diff 算法对比:组件变化时,创建新的虚拟 DOM 树。通过 Diff 算法比较新旧两棵虚拟 DOM 树,精确定位差异。
- Patch(更新):将差异更新到真实 DOM 元素,完成页面更新。
-
优点:
优点 描述 性能优化 避免频繁的直接 DOM 操作,渲染效率高 处理能力强 Diff 算法/PatchFlag 找出差异更高效 易于维护和调试 数据驱动视图,开发体验更好 跨平台能力强 虚拟 DOM 可以用于 Web、SSR、Weex 等 -
缺点:
缺点 描述 占用额外内存 存储 VNode 树,可能造成开销 不适用于 DOM 操作极简或极频繁的场景 比如游戏或动画中,直接操作 DOM 性能可能更好 -
Vue 2 vs Vue 3 的虚拟 DOM 区别:
特性 Vue 2 Vue 3 响应式原理 Object.definePropertyProxy虚拟节点创建 createElementh模板编译输出 未静态提升 静态 DOM 元素提升、静态属性对象预字符串化 性能优化策略 普通 Diff block tree + patchFlag Vue3 的
h函数:用于手动创建 VNode,替代模板语法:import { h } from 'vue'; h('div', { class: 'box' }, 'Hello');
20. Vue 的 Diff 算法
-
Diff 算法:组件更新时,查找新旧虚拟 DOM 差异的方法。
-
流程:
- 生成新的虚拟 DOM 。
- 比较新旧虚拟 DOM ,精确定位差异。
- 将差异更新到真实 DOM 元素。
-
利用
key和tag判断节点是否相同:key:显式指定的唯一标识符。tag:节点的标签名(如div、span或组件名)。- 相同:属性更新、事件更新、子节点对比。
- 不同:销毁旧节点、创建新节点。
-
采用深度优先 + 同层对比 + 双端比较的思路,提高效率。
-
优点:
- 复用已有 DOM 元素,减少创建与销毁操作。
- 更新属性和子节点差异部分,避免全量重绘。
21. Vue 中的 key 的作用
- 虚拟DOM的高效更新:key 作为唯一标识符,帮助 Vue 准确识别节点,提高 Diff 算法精准性,避免不必要的 DOM 更新。
- 组件复用控制:相同类型的组件会尽可能复用,保留内部状态,修改 key 可以强制初始化组件。
- 避免状态错乱:
v-for渲染列表,使用 key 标识唯一项,DOM元素与数据项严格对应,避免状态混乱。
22. $nextTick 的原理与作用(含 DOM 更新与页面渲染)
背景:Vue DOM 更新是异步的
- Vue 修改响应式数据后,不会立刻更新 DOM,而是将更新操作加入异步任务队列,等当前同步代码执行完,再进行批量合并更新。
- 多次对同一响应式数据的修改会被去重合并,只执行最后一次的 DOM 更新。
- DOM 更新之后,浏览器在下一帧(约 16ms)统一渲染(paint)页面,以提升性能。
this.msg = '1'
this.msg = '2'
// Vue 会统一将 msg 改为 2 并更新 DOM,跳过中间的 1
作用
- 等待 DOM 更新完成后,再执行指定的回调函数,确保拿到的是最新的 DOM 状态。
- 应用场景:获取元素位置、高度、focus 输入框、读取文本内容等。
this.msg = 'hello'
this.$nextTick(() => {
console.log(this.$refs.box.innerText) // hello,DOM 已更新
})
DOM 更新与浏览器渲染的关系
- DOM 更新 ≠ 页面立即变化
- Vue 在响应式数据变更后会执行 虚拟 DOM diff 和 patch 操作(内存中)
- 但浏览器并不会立刻渲染这些变化,而是等待任务结束后,在下一帧(frame)内统一执行渲染
✅ 渲染的异步流程如下:
响应式数据变更
↓
虚拟 DOM diff 和 patch 操作
↓
DOM 更新(调用真实 DOM API)
↓
【等待浏览器下次刷新帧】
↓
执行 Layout → Paint → Composite → 显示在屏幕
- 浏览器会自动合并短时间内 DOM 更新,避免频繁重绘,提高性能
$nextTick介于 “Vue 更新 DOM” 与 “浏览器渲染” 之间,确保拿到的是“已经更新过的 DOM 节点”
原理简述
- Vue 内部维护一个回调队列(callbacks),每次
$nextTick会将回调加入队列中。 - 当本轮同步任务结束后,Vue 会通过微任务或宏任务调度执行这些回调函数。
- Vue 使用“微任务优先策略”保证快速且兼容性好的执行效果。
任务调度优先级(降级策略)
| 优先级 | 方法 | 类型 | 说明 |
|---|---|---|---|
| 1️⃣ | Promise.then() | 微任务 | 现代浏览器最快、首选方式 |
| 2️⃣ | MutationObserver | 微任务 | 用于旧版浏览器,如 IE11 |
| 3️⃣ | setImmediate() | 宏任务 | Node.js / IE 独有 |
| 4️⃣ | setTimeout(fn, 0) | 宏任务 | 最终兜底方案,性能最低 |
Vue 2 vs Vue 3 区别对比
| 方面 | Vue 2 | Vue 3 |
|---|---|---|
| 使用方式 | Vue.nextTick / this.$nextTick | import { nextTick } from 'vue' |
| 实现机制 | 使用降级策略(多种异步方案兜底) | 直接使用 Promise.then() 实现 |
| 支持语法 | 回调函数为主 | 支持回调函数 + await 异步语法 |
| Composition API | 不适用 | 推荐配合 async setup() 使用 |
总结
$nextTick是 Vue 提供的一种“等待 DOM 更新后再执行任务”的机制,本质上是利用微任务机制确保拿到的是最新、已 patch 的 DOM,避免操作旧的 DOM 状态。
页面真正发生视觉上的变化(Paint)还要等到下一帧由浏览器统一执行渲染任务,这是浏览器行为,不受 Vue 控制。
23. Vue 3 性能优化
| 优化点 | 运行时收益 |
|---|---|
| 静态提升 | 提出静态节点,只渲染一次 |
| 预字符串化 | 静态属性对象(class、style)预处理为字符串,减少拼接和合并 |
| 缓存事件处理函数 | 稳定函数只创建一次,避免函数重建和 DOM 事件重新绑定 |
| Block Tree(区块树优化) | 跳过整个 DOM 子树 diff |
| PatchFlag(补丁标志位) | 精确定位 DOM 节点变化,跳过无关属性 |
静态提升(Hoist Static)
- 原理:在编译时,提取模板中静态节点,生成静态虚拟节点(vnode),避免更新时重新创建。
- 优点:
- 减少差异对比(diff) 和差异更新 (patch)操作
- 节省内存和运行时间
<template>
<div>
<h1>Hello Vue</h1> <!-- 静态 -->
<p>{{ message }}</p> <!-- 动态 -->
</div>
</template>
<h1>Hello Vue</h1> 将被编译提升为一个静态 vnode,只在首次渲染创建,后续不再参与 patch。
预字符串化(Stringification)
- 原理:在编译时,将模板中静态属性对象(如
class、style),预处理为字符串,避免运行时拼接和合并。 - 优点:
- 减少运行时计算开销
- 渲染性能快
<template>
<div class="static-class" style="color: red">Static</div>
</template>
静态属性对象class、style将被编译为字符串,避免运行时合并类名或样式对象。
缓存事件处理函数(Event Handler Caching)
- 原理:在模板中绑定事件(如 @click="handleClick")为稳定函数(不依赖模板中的响应式数据)时,在首次渲染后缓存起来,后续 patch 复用同一个函数。
- 优点:
- 避免重复创建函数
- 减少 DOM 事件解绑/绑定
<template>
<button @click="handleClick">Click</button>
</template>
handleClick 是稳定函数(不依赖数据),Vue 就不会在每次更新时重新绑定新的事件函数。
Block Tree(区块树优化)
- 原理:在编译时,把动态内容划分Block,只追踪每个 block 的内部变化。在响应式数据变化时,跳过无关 block 的 diff 操作。
- 优点:
- 更新粒度更精确
- 减少渲染成本
<template>
<div>
<p>{{ title }}</p> <!-- 动态内容 -->
<span>Static content</span> <!-- 静态内容 -->
</div>
</template>
把 <p> 和 <span> 分为不同 block,只在 title 变化时更新 <p>。
PatchFlag(补丁标志位)
- 原理:在编译时,为每个动态节点生成一个 PatchFlag(补丁标志),标记它为什么而变,从而在更新时精准定位变动点。
- 优点:
- 精准定位变动点
- 避免全量 diff
<template>
<div :class="isActive ? 'on' : 'off'">Hello</div>
</template>
给节点标记 PatchFlag.CLASS,更新时只关注 class 属性变化,忽略其他内容。
24. Vue2 与 Vue3 的核心区别
| 项目 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | Object.defineProperty | Proxy |
| API 风格 | Options API (固定选项) | Composition API (组合函数) |
| 体积 | 大,不支持 Tree shaking | 小,支持 Tree shaking |
| 性能 | 性能差 | 性能好 |
| 状态管理 | Vuex | 推荐使用 Pinia |
| 类型支持 | 限制多 | 更友好(TypeScript 原生支持) |
| 兼容性 | 老项目支持 | 新项目推荐 Vue 3 |
25. Options API 与 Composition API 区别对比
-
Options API:通过固定的选项(如
data,methods,computed等)分散组织组件逻辑,结构清晰、易上手。 -
Composition API:通过组合函数(
setup())聚合组织逻辑,聚合更灵活,更适合大型项目与逻辑复用。对比点 Options API Composition API 逻辑组织方式 通过固定选项分散组织 data,methods,watch通过组合函数聚合组织 setup中API 引用方式 内部全量引入 通过 import按需引入数据定义位置 data()返回对象ref()或reactive()在setup()中定义方法定义方式 methods: {}在 setup()中定义函数模板访问变量 自动暴露 data,methods,computed中的内容必须在 setup()返回代码复用 Mixin(容易冲突) 组合函数,提高复用性 TypeScript 支持 支持差 原生支持类型推导和 TS Tree shaking 不支持 支持,可减小打包体积 可读性 结构清晰 逻辑灵活
26. Vue2 的 Mixin(混入)
Mixin 定义
Mixin 是包含组件的 data、methods、computed、watch、生命周期钩子函数等选项的对象。用以封装多个组件中可复用的功能逻辑代码,混入组件后按特定规则合并。提高了代码复用性,但也存在命名冲突、不利于逻辑聚合等问题。
// 定义一个 mixin
const myMixin = {
data() {
return {
sharedData: 'hello'
}
},
created() {
console.log('Mixin created')
},
methods: {
greet() {
console.log('Hello from mixin')
}
}
}
// 使用 mixin 的组件
export default {
mixins: [myMixin],
}
选项合并规则
- data:合并,用 set() 进行合并和重新赋值。
- methods、computed:替换,同名时组件的定义优先。
- 生命周期钩子、watch:队列合并,合并成数组,并按顺序依次调用(先调用 mixin 的,再调用组件本身的)。
优点 ✅
- 逻辑复用方便,开发效率高。
- 代码更简洁,便于维护公共逻辑。
缺点 ❌
- 命名冲突:多个 mixin 可能导致变量和方法命名冲突,覆盖关系不清晰。
- 来源不明:组件中变量和方法难以直接确定来源(debug 不便)。
- 可读性差:不同 mixin 的逻辑分散,不利于理解业务流程和代码调试。
与 Vue3 Composition API 对比
Vue3 推荐使用 Composition API 替代 mixin,因为:
- 不存在命名冲突问题。
- 逻辑更清晰聚合,便于重用和组合。
- 更容易进行类型推导和测试。
27. Vue 实现 CSS scoped 的原理
- 编译时,为组件内每个 DOM 元素添加唯一的
data-v-xxxxxx属性。 - 同步改写 CSS 选择器样式,限制样式作用范围在当前组件,避免样式污染。
示例编译后:
<h1 data-v-abc123>Hello</h1>
h1[data-v-abc123] { color: red; }
28. 在 Vue 2 中,组件的 data 为什么是一个函数而不是对象?
确保组件每次实例化都创建新对象,组件数据相互独立,避免不可预测的副作用。
data() {
return {
count: 0
};
}
1. 避免数据共享污染
- 根本问题:对象是 引用类型。
- 如果直接使用对象形式的
data,所有组件实例将 共享同一个数据对象。 - 修改一个实例的数据,实例间互相污染,会导致其他实例的数据同步变化(因为它们引用同一内存地址)。
- 如果直接使用对象形式的
2. 确保数据独立性
- 解决方案:
data定义为函数,每次返回 新对象。- 每个组件实例调用
data函数,获得独立的数据副本。 - 数据隔离,避免交叉污染。
- 每个组件实例调用
3. 根实例为何允许对象?
-
根实例唯一性:
- 通过
new Vue()创建的根实例是 单例,不会被复用。 - 使用对象形式
data不会导致数据共享问题。
- 通过
-
示例:
// ✅ 根实例可以使用对象 new Vue({ el: '#app', data: { message: 'Hello' } // 安全,因为根实例唯一 });
29. Vue 组件与插件的区别
组件与插件的核心区别
| 特性 | 组件(Component) | 插件(Plugin) |
|---|---|---|
| 功能目标 | 封装 UI 和局部逻辑的 Vue 实例 | 扩展 Vue 全局能力或集成第三方库 |
| 作用范围 | 局部或全局 UI 模块 | 全局功能增强 |
| 注册方式 | components 选项或 Vue.component() | Vue.use() 安装 |
| 代码结构 | 包含模板、样式、逻辑的独立文件 | 通常是一个包含 install 方法的对象 |
| 典型示例 | 按钮、表单、弹窗 | Vuex、Vue Router、自定义工具库 |
常见误区
- 插件提供组件:某些插件会注册全局组件(如 Element UI 的
<el-button>),但插件本身的核心功能是全局扩展。 - 组件复用 vs 插件复用:
- 组件复用 UI 和局部逻辑。
- 插件复用全局功能(如埋点、鉴权)。
总结
- 组件:用于构建应用界面,通过组合形成页面结构。
- 插件:用于扩展 Vue 全局能力,提供跨组件共享的功能。
- 协作关系:插件可以为组件提供全局支持(如路由、状态管理),而组件是插件功能的具体载体。
30. SPA(Single Page Application)(单页面应用)
什么是 SPA?
SPA(Single Page Application) 是指整个网站只有一个 HTML 页面,用户在使用过程中页面不会重新加载,而是通过前端路由和异步请求动态渲染内容。
特征:
- 页面首次加载时,请求一个完整的 HTML + JS 应用
- 后续页面跳转由前端路由控制(如 Vue Router)
- 通过 AJAX/FETCH 获取数据,DOM 动态更新
SPA 的工作原理
- 首次加载:只加载一次
index.html,并通过 JavaScript 渲染页面结构。 - 路由变化:使用
history.pushState()或hashchange监听地址栏变化。 - 组件切换:根据路由加载不同组件,更新视图而不刷新整个页面。
- 数据交互:通过 Ajax 或 Fetch 与后端 API 通信,动态获取数据渲染到页面。
SPA 的优点 ✅
| 优点 | 说明 |
|---|---|
| 用户体验流畅 | 页面切换无需刷新整个页面 |
| 前后端分离 | 前端负责视图和交互,后端提供纯数据 API,易于协作开发 |
| 客户端缓存 | 已加载过的组件/数据可缓存,提升性能 |
SPA 的缺点 ❌
| 缺点 | 说明 |
|---|---|
| 首次加载慢 | 要加载大量 JS 和资源,影响首屏时间 |
| SEO 不友好(Vue2时代) | 纯前端渲染的内容对搜索引擎不友好(可通过 SSR 解决) |
| 浏览器前进/后退处理复杂 | 需手动处理路由状态管理 |
31. SPA 如何提升首屏加载速度
资源加载优化
目标:减少首屏资源体积,提升加载效率
-
资源懒加载(按需加载)
- 路由懒加载:使用
import()动态导入路由组件,避免首屏加载所有页面代码。 - 组件懒加载:只加载当前页面使用的 UI 组件,避免全量引入组件库。
- 图片懒加载:使用原生
loading="lazy"延迟图片加载,减少请求压力。
- 路由懒加载:使用
-
静态资源缓存
- 静态资源版本化,通过 hash 命名识别资源是否需要更新,实现文件长期缓存。(Vite默认带,Webpack需配置)
- 配合 Service Worker 控制关键资源缓存策略(如首页 shell、主 JS、路由文件等)。
-
资源压缩与优化
- 使用 Gzip/Brotli 对 JS/CSS 压缩
- 图片使用 WebP 格式、SVG 替代位图图标
-
代码分割(Code Splitting)
- 将大模块(如 ECharts、Lodash)打包成独立 chunk,提高缓存复用率,降低主包体积。
- 配合 Webpack/Vite 自动按模块拆分。
-
预加载与预获取
<link rel="preload">:提前加载当前页面关键资源(如字体、首屏 JS)。<link rel="prefetch">:在浏览器空闲时预取下一个可能访问的页面资源。
页面渲染优化
目标:更早展示页面结构,减少“白屏时间”
-
使用 SSR(服务端渲染)
- 在服务端生成首屏 HTML,用户打开页面时即可看到内容,提升首屏体验与 SEO 效果。
- Vue 可通过 Nuxt.js 实现 SSR。
-
骨架屏 / Loading 占位
- 在页面加载过程中渲染骨架结构或 loading 动画,提升用户感知速度。
-
异步数据优化
- 首屏只请求必要接口,次要数据通过懒加载或分页获取,减轻首屏数据压力。
32. SSR(服务端渲染)
什么是 SSR?
-
SSR 是服务端渲染技术,可以在服务器将 Vue 页面直接渲染为 HTML 字符串,直接返回给浏览器,从而解决 SPA 白屏时间长和 SEO 不友好的问题。
-
它提升了首屏加载速度,改善了搜索引擎爬取效果,但也增加了服务端压力和开发复杂度。
-
在 Vue2 常通过 Nuxt.js 实现 SSR,Vue 3 使用
createSSRApp。
SSR 解决了哪些问题?
| 问题 | SSR 的解决方式 |
|---|---|
| 首屏加载白屏 | SSR 提前渲染 HTML,用户可以立即看到页面结构内容 |
| 提升首屏加载速度 | 服务器直接渲染数据,避免前端先下载 JS 再请求数据再渲染的过程 |
| SEO 不友好 | SSR 输出完整 HTML,爬虫可以正常抓取页面信息(特别是百度) |
✅ 优点
- 更快的首屏渲染,提升用户体验
- 更好地支持 SEO(搜索引擎优化)
- 支持分享链接的预览信息展示(如微信分享卡片)
❌ 缺点
- 实现成本高:涉及 Node.js 服务、路由匹配、异步数据预取
- 服务端压力大:每个请求都要计算 HTML,难以缓存
- 开发复杂度增加:需要考虑客户端/服务端的通用代码处理
33. Tree Shaking(摇树优化)
什么是 Tree Shaking?
Tree Shaking 是一种在打包/构建阶段,通过移除代码中未使用部分,从而 减小打包体积,提升加载速度的技术。
Tree Shaking 工作原理
- 基于 ES Module(
import/export),静态分析导入导出关系。 - 常用于 webpack、Vite 等构建工具,扫描哪些模块的导出部分没有被使用。
- 移除未使用的导出部分,不打包到最终输出文件中。
// utils.js
export function add(a, b) { return a + b }
export function multiply(a, b) { return a * b }
// index.js
import { add } from './utils.js'
console.log(add(2, 3))
这里
multiply()没被用,构建时就会被 Tree Shaking 掉。
Tree Shaking 适用场景
- 移除未使用的工具函数(如 lodash、自定义工具库)
- 移除 UI 组件库中未用的组件(如 Element UI、AntD)
- 配合动态导入(懒加载)优化大型页面或库(如 ECharts)
Tree Shaking vs Code Splitting
| 技术 | 定义 | 作用 |
|---|---|---|
| Tree Shaking(摇树优化) | 移除代码中未被使用的部分 | 减少打包体积 |
| Code Splitting(代码分割) | 将代码拆分多个模块,按需加载 | 提升首屏加载速度 |