一、基础概念篇
1. 什么是Vue2?它的核心特点有哪些?
你想了解的Vue2是一套用于构建用户界面的渐进式JavaScript框架(发布于2016年,目前仍有大量存量项目在使用),所谓“渐进式”指的是可以按需引入Vue的功能模块,无需一次性全盘接受,适合从简单页面到复杂单页应用(SPA)的逐步迭代。
它的核心特点如下:
- 数据驱动视图(MVVM思想):无需手动操作DOM,只需维护数据,Vue会自动完成数据到DOM的映射更新
- 组件化开发:将页面拆分为可复用、独立的组件,提高开发效率和代码可维护性
- 双向数据绑定(v-model):简化表单交互,实现数据与视图的双向同步
- 虚拟DOM:通过抽象DOM结构减少真实DOM操作,提高渲染性能
- 指令系统:提供一系列内置指令(如v-if、v-for),简化DOM操作逻辑
- 生命周期钩子:提供完整的组件生命周期回调,方便在不同阶段执行业务逻辑
2. Vue2与Vue3的核心区别是什么?(高频面试题)
两者的核心差异集中在底层架构、性能、语法体验三个方面,核心区别如下:
| 对比维度 | Vue2 | Vue3 |
|---|---|---|
| 核心架构 | 选项式API(Options API),按属性分类(data、methods、computed) | 组合式API(Composition API),按业务逻辑组织代码,更适合复杂组件 |
| 响应式原理 | Object.defineProperty(),无法监听对象新增属性、数组下标修改 | Proxy + Reflect,全方位监听对象/数组变化,无需特殊处理 |
| 虚拟DOM | 传统虚拟DOM,更新粒度较粗 | 优化后的虚拟DOM,结合编译期优化,更新效率更高 |
| 组件写法 | 单根节点限制 | 支持多根节点(片段) |
| 生命周期 | beforeCreate/created等(无setup) | 新增setup钩子,对应替换beforeCreate/created,其余钩子添加on前缀(如onMounted) |
| 体积 | 相对较大,无法按需引入核心功能 | 支持Tree-Shaking,体积更小,按需引入API |
3. Vue2中的MVVM模式是什么?如何理解?
MVVM是Model-View-ViewModel的缩写,是一种软件架构模式,Vue2正是基于MVVM思想实现的,各部分职责如下:
-
Model(模型) :对应Vue中的
data、props等数据层,存储业务数据和状态,不关心视图展示 -
View(视图) :对应DOM模板层,负责展示数据,是被动的,仅根据Model数据更新自身
-
ViewModel(视图模型) :Vue实例本身就是ViewModel,作为Model和View之间的桥梁,核心职责:
- 监听Model数据变化(通过
Object.defineProperty()) - 当Model数据变化时,更新View(数据驱动视图)
- 监听View的用户交互(如表单输入),更新Model数据(视图驱动数据,双向绑定)
- 监听Model数据变化(通过
-
核心优势:解耦View和Model,开发者无需操作DOM,只需专注于数据和业务逻辑。
二、核心语法篇
1. Vue2中如何实现数据响应式?有什么局限性?
实现原理
Vue2通过Object.defineProperty()方法对data中的属性进行数据劫持,结合发布-订阅模式实现响应式,核心步骤如下:
- 初始化时,遍历
data中的所有属性(递归遍历嵌套对象) - 对每个属性使用
Object.defineProperty()定义getter和setter getter:当属性被访问时,收集依赖(即当前使用该属性的组件/Watcher)setter:当属性值被修改时,通知所有收集到的依赖,触发对应的更新逻辑,更新视图
局限性
- 无法监听对象新增的属性(只能劫持初始化时已存在的属性)
- 无法监听数组的下标修改和长度修改(如
arr[0] = 10、arr.length = 0) - 对于复杂嵌套对象,递归劫持会有一定的性能开销
解决方案
- 新增对象属性:使用
Vue.set(object, key, value)或this.$set(object, key, value) - 修改数组:使用Vue封装的数组方法(
push、pop、shift、unshift、splice、sort、reverse),或Vue.set(arr, index, value) - 替换整个对象/数组:直接重新赋值(如
this.obj = { ...this.obj, newKey: newValue })
2. v-model的工作原理是什么?如何在自定义组件上使用v-model?
工作原理
v-model是Vue提供的语法糖,本质是结合了v-bind(绑定value属性)和v-on(监听input事件),核心逻辑如下:
<!-- 原生表单元素上的v-model -->
<input v-model="message">
<!-- 等价于手动绑定和监听 -->
<input :value="message" @input="message = $event.target.value">
对于不同的表单元素,v-model对应的属性和事件略有差异:
- 输入框(text/textarea):
value属性 +input事件 - 单选框/复选框:
checked属性 +change事件 - 下拉框(select):
value属性 +change事件
自定义组件上使用v-model
自定义组件默认通过value属性接收数据,通过input事件传递更新后的数据,步骤如下:
- 子组件:通过
props接收value属性 - 子组件:当内部数据变化时,通过
$emit('input', 新值)触发事件 - 父组件:直接在子组件标签上使用
v-model绑定数据即可
示例代码:
<!-- 子组件 Child.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)">
</template>
<script>
export default {
props: ['value'] // 接收父组件传递的value
}
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child v-model="parentMessage"></Child>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
parentMessage: ''
}
}
}
</script>
如果想修改默认的value和input,可以通过子组件的model选项配置:
// 子组件中配置
export default {
model: {
prop: 'msg', // 替换默认的value
event: 'updateMsg' // 替换默认的input
},
props: ['msg']
}
3. v-for和v-if为什么不建议同时使用?有什么替代方案?
不建议同时使用的原因
v-for的优先级高于v-if,当两者同时作用在同一个元素上时,会导致性能问题:
- 每次渲染时,都会先执行
v-for遍历整个数组,再对每个遍历项执行v-if判断 - 即使只有少量数据需要展示,也会遍历全部数据,造成不必要的性能开销
- 逻辑上不清晰,
v-for用于遍历,v-if用于条件判断,分离职责更易维护
替代方案
- 使用计算属性(推荐) :先通过计算属性过滤掉不需要展示的数据,再进行
v-for遍历
<template>
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Vue2', active: true },
{ id: 2, name: 'Vue3', active: false }
]
}
},
computed: {
activeItems() {
// 先过滤,再遍历
return this.items.filter(item => item.active)
}
}
}
</script>
- 在外层包裹元素上使用v-if:如果是整体显示/隐藏,而非单个遍历项,可在外层加
v-if
<template>
<div v-if="showItems">
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</div>
</template>
4. Vue2中的指令有哪些分类?常用指令的作用是什么?
Vue2的指令以v-开头,用于在视图上添加特殊行为,主要分为以下几类:
1. 数据绑定指令
v-text:渲染纯文本,替代{{ }},不会解析HTML(比{{ }}更安全,无闪烁问题)v-html:渲染HTML内容,存在XSS安全风险,不建议在用户输入内容上使用v-bind:绑定HTML属性或组件props,缩写为:,如:src="imgUrl"、:class="className"v-model:双向数据绑定,仅适用于表单元素和自定义组件
2. 条件渲染指令
v-if:根据条件动态创建/销毁元素,惰性渲染(初始条件为假时,不渲染)v-else:配合v-if使用,无独立条件,对应v-if的相反场景v-else-if:配合v-if使用,多条件判断v-show:根据条件显示/隐藏元素(修改CSS的display属性),初始渲染时无论条件真假都会渲染
3. 列表渲染指令
-
v-for:遍历数组、对象、字符串、数字进行渲染,必须配合key属性使用(提高diff算法效率)- 遍历数组:
v-for="(item, index) in arr" :key="item.id" - 遍历对象:
v-for="(value, key, index) in obj" :key="key"
- 遍历数组:
4. 事件绑定指令
-
v-on:绑定DOM事件或组件自定义事件,缩写为@,如@click="handleClick"、@input="handleInput"- 支持事件修饰符:
.stop(阻止冒泡)、.prevent(阻止默认行为)、.once(只触发一次)等 - 支持按键修饰符:
.enter(回车)、.tab(制表符)等
- 支持事件修饰符:
5. 其他常用指令
v-cloak:解决{{ }}插值表达式的闪烁问题(配合CSS[v-cloak] { display: none; }使用)v-once:只渲染一次,后续数据变化不会重新渲染,用于静态内容优化性能v-pre:跳过当前元素及其子元素的编译过程,直接显示原始内容,用于优化大量静态内容的渲染
5. 计算属性(computed)和侦听器(watch)的区别是什么?各自的使用场景?
核心区别
| 对比维度 | computed(计算属性) | watch(侦听器) |
|---|---|---|
| 本质 | 基于依赖缓存的属性,返回一个计算结果 | 监听数据变化,执行自定义逻辑(无返回值) |
| 缓存机制 | 依赖的数据不变时,多次访问会返回缓存结果,不重复计算 | 无缓存,只要监听的数据变化,就会执行回调 |
| 语法形式 | 声明式语法,像使用普通属性一样调用(无需加括号) | 命令式语法,需要定义回调函数 |
| 适用场景 | 简单的同步计算,依赖多个数据推导一个结果 | 复杂的异步操作、数据变化后的后续逻辑(如请求接口) |
| 参数 | 无参数,自动依赖data/props中的数据 | 可接收两个参数(新值、旧值) |
各自的使用场景
-
computed 适用场景
- 数据格式化(如将时间戳转为日期字符串)
- 多数据联动计算(如购物车总价 = 商品单价 * 数量 + 运费)
- 数据过滤/转换(如将数组按条件过滤后返回新数组)
示例:
computed: {
// 购物车总价
totalPrice() {
return this.goods.reduce((total, item) => total + item.price * item.quantity, 0) + this.freight
}
}
-
watch 适用场景
- 监听数据变化,发起异步请求(如根据搜索关键词请求接口数据)
- 数据变化后执行复杂的业务逻辑(如表单数据变化后验证表单)
- 监听路由变化(
$route),执行页面跳转后的逻辑
示例:
watch: {
// 监听搜索关键词,发起接口请求
searchKey(newVal, oldVal) {
if (newVal) {
// 异步请求
this.$axios.get(`/api/search?keyword=${newVal}`).then(res => {
this.searchResult = res.data
})
}
},
// 深度监听对象内部属性变化
userInfo: {
handler(newVal) {
console.log('用户信息变化', newVal)
},
deep: true, // 开启深度监听
immediate: true // 初始化时立即执行一次回调
}
}
三、组件化开发篇
1. Vue2中组件的通信方式有哪些?分别适用于什么场景?
Vue2组件通信分为父子组件通信、兄弟组件通信、跨层级/全局通信三大类,具体方式及场景如下:
1. 父子组件通信(最常用)
-
方式1:父传子(props)
- 原理:父组件通过
v-bind传递数据,子组件通过props接收数据 - 适用场景:父组件向子组件传递静态/动态数据、配置项
- 注意:
props是单向数据流,子组件不能直接修改props,如需修改,可通过$emit通知父组件修改
- 原理:父组件通过
示例:
<!-- 父组件 -->
<Child :title="pageTitle" :list="goodsList"></Child>
<!-- 子组件 -->
<script>
export default {
props: {
// 类型验证 + 默认值
title: {
type: String,
default: '默认标题'
},
list: {
type: Array,
default: () => [] // 数组/对象默认值需用函数返回,避免共享引用
}
}
}
</script>
-
方式2:子传父($emit / 自定义事件)
- 原理:子组件通过
this.$emit('事件名', 传递数据)触发事件,父组件通过v-on监听事件并接收数据 - 适用场景:子组件向父组件传递用户交互结果、数据更新通知
- 原理:子组件通过
示例:
<!-- 子组件 -->
<button @click="handleClick">点击传递数据</button>
<script>
export default {
methods: {
handleClick() {
// 触发自定义事件,传递数据
this.$emit('child-data', { id: 1, name: 'Vue2' })
}
}
}
</script>
<!-- 父组件 -->
<Child @child-data="handleChildData"></Child>
<script>
export default {
methods: {
handleChildData(data) {
console.log('接收子组件数据', data)
}
}
}
</script>
-
方式3:父组件直接访问子组件($refs)
- 原理:父组件通过
ref给子组件命名,通过this.$refs.xxx访问子组件的属性和方法 - 适用场景:父组件需要主动调用子组件的方法(如子组件的重置表单方法)
- 注意:
$refs仅在组件挂载完成后生效,无法在created钩子中访问
- 原理:父组件通过
2. 兄弟组件通信
-
方式1:通过父组件中转
- 原理:兄弟A通过
$emit将数据传递给父组件,父组件再通过props传递给兄弟B - 适用场景:兄弟组件关系简单,层级较浅
- 原理:兄弟A通过
-
方式2:通过EventBus(事件总线)
-
原理:创建一个全局的Vue实例作为事件中心,兄弟组件通过
$on监听事件,$emit触发事件 -
适用场景:兄弟组件较多、层级较深,无需父组件中转
-
步骤:
-
创建事件总线(
src/utils/bus.js)-
import Vue from 'vue' export default new Vue()
-
-
发送数据的组件(兄弟A)
-
import Bus from '@/utils/bus' Bus.$emit('brother-data', { msg: '来自兄弟A的数据' })
-
-
接收数据的组件(兄弟B)
-
import Bus from '@/utils/bus' export default { mounted() { // 监听事件 this.busListener = Bus.$on('brother-data', data => { console.log('接收兄弟A数据', data) }) }, beforeDestroy() { // 销毁事件监听,防止内存泄漏 Bus.$off('brother-data', this.busListener) } }
-
-
-
3. 跨层级/全局通信
-
方式1:Vuex/Pinia(推荐)
- 原理:集中式状态管理库,将全局共享数据存储在state中,组件通过
dispatch/commit修改数据,通过mapState/getters获取数据 - 适用场景:中大型项目,大量组件共享全局数据(如用户信息、购物车数据)
- 原理:集中式状态管理库,将全局共享数据存储在state中,组件通过
-
方式2:provide / inject(依赖注入)
-
原理:父组件通过
provide提供数据,所有后代组件(无论层级多深)都可以通过inject注入数据 -
适用场景:深层级组件通信,无需逐层传递
props -
注意:非响应式(默认),如需响应式,可提供一个包含响应式数据的对象
-
示例:
-
// 父组件 export default { provide() { return { // 提供响应式数据(绑定this,确保数据更新) userInfo: this.userInfo } }, data() { return { userInfo: { name: '张三', age: 25 } } } } // 后代组件 export default { inject: ['userInfo'], // 注入数据 mounted() { console.log('接收父组件提供的数据', this.userInfo) } }
-
-
-
方式3:全局变量(Vue.prototype / window)
-
原理:将数据挂载到
Vue.prototype(全局Vue实例)或window上,所有组件均可访问 -
适用场景:简单的全局配置(如接口基础地址、全局常量)
-
示例:
-
// main.js import Vue from 'vue' Vue.prototype.$baseUrl = 'https://api.example.com' // 组件中访问 console.log(this.$baseUrl)
-
-
2. Vue2中组件的props验证有哪些方式?如何使用?
为了提高组件的健壮性和可维护性,Vue2支持对props进行类型、格式、必要性等验证,验证方式分为简单验证和复杂验证。
1. 简单验证(直接指定类型)
适用于简单场景,直接指定props的类型(String、Number、Boolean、Array、Object、Function、Symbol)。
export default {
props: {
title: String, // 字符串类型
count: Number, // 数字类型
isShow: Boolean, // 布尔类型
list: Array, // 数组类型
config: Object, // 对象类型
handleClick: Function // 函数类型
}
}
2. 复杂验证(对象形式,包含多属性配置)
适用于复杂场景,可配置type(类型)、required(是否必传)、default(默认值)、validator(自定义验证函数)。
export default {
props: {
// 必传的字符串类型
name: {
type: String,
required: true, // 必传,未传递会在控制台报警告
default: '未知名称' // 仅在required为false时生效
},
// 数字类型,有默认值
age: {
type: Number,
default: 18
},
// 数组类型,默认值需用函数返回(避免多个组件共享同一引用)
hobbies: {
type: Array,
default: () => ['读书', '运动']
},
// 对象类型,默认值需用函数返回
userInfo: {
type: Object,
default: () => ({
id: '',
name: ''
})
},
// 自定义验证函数
score: {
type: Number,
validator: (value) => {
// 验证分数是否在0-100之间
return value >= 0 && value <= 100
},
message: '分数必须在0到100之间' // 自定义报错信息(Vue2.x需配合第三方插件,默认控制台显示默认信息)
}
}
}
注意事项
type可以是单个类型,也可以是多个类型的数组(如type: [String, Number])default对于基本类型可以直接赋值,对于数组/对象必须用函数返回,避免组件间共享数据引用- 验证失败时,Vue会在控制台抛出警告(开发环境),但不会阻止程序运行
props验证仅在开发环境生效,生产环境会被忽略,以提高性能
3. Vue2中如何实现组件的插槽(Slot)?有哪些类型的插槽?
插槽(Slot)是Vue实现的内容分发机制,允许父组件向子组件的指定位置插入自定义内容,提高组件的灵活性和复用性,Vue2支持三种插槽类型:默认插槽、具名插槽、作用域插槽。
1. 默认插槽(匿名插槽)
-
原理:子组件中用
<slot></slot>定义插槽位置,父组件直接在子组件标签内写入内容,会自动填充到默认插槽中 -
适用场景:子组件只有一个插槽,无需区分位置
-
示例:
-
<!-- 子组件 Child.vue --> <template> <div class="child-container"> <h3>子组件标题</h3> <!-- 默认插槽 --> <slot>默认内容(父组件未传递内容时显示)</slot> </div> </template> <!-- 父组件 Parent.vue --> <template> <Child> <!-- 插入到默认插槽中的内容 --> <p>这是父组件传递给子组件的默认插槽内容</p> </Child> </template>
-
2. 具名插槽
-
原理:子组件通过
slot的name属性定义多个具名插槽,父组件通过v-slot:name(缩写#name)指定内容插入到对应的插槽中 -
适用场景:子组件有多个插槽,需要区分不同位置的内容
-
示例:
-
<!-- 子组件 Child.vue --> <template> <div class="child-container"> <!-- 头部插槽 --> <slot name="header"></slot> <!-- 主体插槽(默认插槽,name可省略) --> <slot></slot> <!-- 底部插槽 --> <slot name="footer"></slot> </div> </template> <!-- 父组件 Parent.vue --> <template> <Child> <!-- 具名插槽:头部 --> <template #header> <h1>这是页面头部</h1> </template> <!-- 默认插槽 --> <p>这是页面主体内容</p> <!-- 具名插槽:底部 --> <template #footer> <div>这是页面底部</div> </template> </Child> </template>
-
3. 作用域插槽
-
原理:子组件通过
slot绑定属性(传递数据),父组件在插槽中通过v-slot接收子组件传递的数据,实现“子传父”的内容分发 -
适用场景:父组件需要控制插槽内容的渲染样式,但数据来源于子组件(如自定义列表项的渲染)
-
示例:
-
<!-- 子组件 Child.vue --> <template> <div class="child-container"> <ul> <li v-for="item in list" :key="item.id"> <!-- 插槽绑定子组件数据,传递给父组件 --> <slot :item="item" :index="index"></slot> </li> </ul> </div> </template> <script> export default { data() { return { list: [ { id: 1, name: 'Vue2', author: '尤雨溪' }, { id: 2, name: 'React', author: 'Facebook' } ] } } } </script> <!-- 父组件 Parent.vue --> <template> <Child> <!-- 接收子组件传递的插槽数据 --> <template v-slot="slotProps"> <!-- 自定义渲染样式,使用子组件数据 --> <span>{{ slotProps.index + 1 }}. {{ slotProps.item.name }} - {{ slotProps.item.author }}</span> </template> </Child> </template> - 简化写法:解构赋值
-
<template v-slot="{ item, index }"> <span>{{ index + 1 }}. {{ item.name }} - {{ item.author }}</span> </template>
-
四、生命周期篇
1. Vue2组件的生命周期钩子有哪些?各自的执行时机和作用是什么?
Vue2组件的生命周期分为四个阶段,共8个核心钩子函数,执行顺序固定,每个钩子对应不同的组件状态,具体如下:
阶段1:初始化阶段(组件创建前/后,未挂载到DOM)
-
beforeCreate(创建前)
- 执行时机:Vue实例初始化之后,数据劫持(
data/props)和事件配置之前 - 组件状态:
data、props、methods、$el均未初始化,无法访问 - 作用:极少使用,可用于执行一些无需依赖组件数据的初始化操作(如全局事件监听的初始化)
- 执行时机:Vue实例初始化之后,数据劫持(
-
created(创建后)
- 执行时机:Vue实例初始化完成,数据劫持、事件配置已完成,但DOM未生成(
$el仍为undefined) - 组件状态:可访问
data、props、methods,但无法访问DOM元素 - 作用:常用,用于初始化数据、发起异步请求(如获取组件初始化所需的接口数据)、监听事件
- 执行时机:Vue实例初始化完成,数据劫持、事件配置已完成,但DOM未生成(
阶段2:挂载阶段(组件挂载到DOM)
-
beforeMount(挂载前)
- 执行时机:模板编译完成,即将开始挂载DOM(
$el已生成,但未挂载到页面) - 组件状态:可访问
data、props,$el已存在,但对应的DOM元素尚未渲染到页面 - 作用:可用于在DOM挂载前修改组件数据,不会触发额外的渲染
- 执行时机:模板编译完成,即将开始挂载DOM(
-
mounted(挂载后)
- 执行时机:组件已成功挂载到页面DOM中,模板渲染完成
- 组件状态:可访问DOM元素(
$el、$refs),数据已完成渲染 - 作用:常用,用于操作DOM(如初始化第三方插件、获取DOM元素尺寸、绑定DOM事件)
阶段3:更新阶段(组件数据变化,触发视图更新)
-
beforeUpdate(更新前)
- 执行时机:组件
data/props数据变化,虚拟DOM重新渲染之前 - 组件状态:数据已更新,但视图尚未更新(仍为旧的DOM)
- 作用:可用于在视图更新前获取旧的DOM状态,或取消一些不必要的更新
- 执行时机:组件
-
updated(更新后)
- 执行时机:虚拟DOM重新渲染完成,视图已更新为最新数据
- 组件状态:数据和视图均已更新,可访问最新的DOM元素
- 作用:可用于在视图更新后执行DOM操作,但需避免在此修改
data(会导致无限更新循环)
阶段4:销毁阶段(组件从DOM中卸载)
-
beforeDestroy(销毁前)
- 执行时机:组件即将被销毁,实例仍处于可用状态
- 组件状态:
data、props、methods、DOM均仍可访问 - 作用:常用,用于清理资源(如取消全局事件监听、清除定时器、销毁第三方插件实例),防止内存泄漏
-
destroyed(销毁后)
- 执行时机:组件已被完全销毁,实例所有属性和方法均已失效,DOM已从页面中移除
- 组件状态:无法访问
data、props、DOM及组件实例 - 作用:极少使用,可用于执行最终的清理操作
生命周期执行顺序总结
beforeCreate → created → beforeMount → mounted → (数据变化触发)beforeUpdate → updated → (组件销毁触发)beforeDestroy → destroyed
2. 父子组件的生命周期执行顺序是怎样的?
父子组件的生命周期执行遵循**“先父后子,先挂载后销毁”**的原则,具体顺序如下:
1. 挂载阶段(初始化渲染)
父beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted
- 解析:父组件先完成自身的初始化,再开始子组件的初始化和挂载,子组件挂载完成后,父组件才最终完成挂载
2. 更新阶段(父/子组件数据变化)
-
情况1:父组件数据变化,触发更新
-
父beforeUpdate → 子beforeUpdate → 子updated → 父updated
-
-
情况2:子组件数据变化,触发更新
-
子beforeUpdate → 子updated
-
-
解析:父组件更新会带动子组件更新,子组件先完成更新,父组件再完成更新;子组件自身数据变化仅触发子组件的更新钩子
3. 销毁阶段(父组件销毁)
父beforeDestroy → 子beforeDestroy → 子destroyed → 父destroyed
- 解析:父组件开始销毁前,先销毁所有子组件,子组件完全销毁后,父组件再完成自身销毁
3. 为什么不建议在created钩子中操作DOM?为什么在mounted钩子中可以?
-
created钩子中无法操作DOM的原因:
created钩子执行时,Vue实例仅完成了data、props、methods的初始化,模板编译尚未完成,$el(组件根DOM元素)尚未生成,更未将DOM挂载到页面中- 此时页面中还不存在当前组件对应的DOM元素,操作DOM会返回
null或undefined,无法达到预期效果
-
mounted钩子中可以操作DOM的原因:
mounted钩子执行时,Vue已完成模板编译,生成了对应的虚拟DOM,并将虚拟DOM渲染为真实DOM,成功挂载到页面的DOM树中- 此时
$el已指向组件的根DOM元素,$refs也已绑定对应的子DOM元素,能够正常访问和操作DOM,适合初始化第三方依赖DOM的插件(如ECharts、Element UI的弹窗等)
示例:
export default {
created() {
// 无法操作DOM,this.$el 为 undefined
console.log(this.$el) // undefined
document.getElementById('app') // 可能为 null(若组件未挂载)
},
mounted() {
// 可以正常操作DOM
console.log(this.$el) // 组件根DOM元素
this.initEcharts() // 初始化ECharts图表(依赖DOM)
}
}
五、状态管理篇(Vuex)
1. Vuex是什么?它的核心组成部分有哪些?各自的作用是什么?
什么是Vuex
Vuex是专为Vue.js应用设计的集中式状态管理库,用于解决组件间全局数据共享和状态管理的问题,它采用单向数据流的设计,将全局共享的数据集中存储和管理,确保数据变更的可预测性和可维护性。
核心组成部分(5个核心模块)
Vuex的核心是Store(仓库),包含State、Getter、Mutation、Action、Module五个部分,各自作用如下:
-
State(状态)
- 作用:存储全局共享的核心数据(如用户信息、购物车数据、全局配置)
- 特点:单一状态树(一个应用只有一个
Store,所有全局状态都存储在State中) - 访问方式:组件中通过
this.$store.state.xxx或mapState辅助函数访问
示例:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
userInfo: null, // 用户信息
cartList: [] // 购物车列表
}
})
-
Getter(派生状态)
- 作用:类似于组件的
computed,对State中的数据进行加工处理(过滤、转换、计算),返回派生数据,且具有缓存机制 - 特点:依赖
State数据,State数据变化时,Getter会重新计算 - 访问方式:组件中通过
this.$store.getters.xxx或mapGetters辅助函数访问
- 作用:类似于组件的
示例:
export default new Vuex.Store({
state: { cartList: [] },
getters: {
// 购物车商品总数
cartTotalCount: state => {
return state.cartList.reduce((total, item) => total + item.quantity, 0)
},
// 带参数的Getter(返回一个函数)
cartItemById: state => (id) => {
return state.cartList.find(item => item.id === id)
}
}
})
-
Mutation(同步修改状态)
- 作用:唯一允许修改
State数据的方式,用于执行同步的状态修改逻辑 - 特点:必须是同步函数(异步操作会导致状态变更不可追踪,无法进行DevTools调试)
- 触发方式:组件中通过
this.$store.commit('mutation名', 载荷)或mapMutations辅助函数触发
- 作用:唯一允许修改
示例:
export default new Vuex.Store({
state: { cartList: [] },
mutations: {
// 添加商品到购物车
ADD_TO_CART(state, payload) {
state.cartList.push(payload)
},
// 修改商品数量
UPDATE_CART_QUANTITY(state, { id, quantity }) {
const item = state.cartList.find(item => item.id === id)
if (item) item.quantity = quantity
}
}
})
-
Action(异步操作/批量修改状态)
- 作用:用于执行异步操作(如接口请求、定时器),异步操作完成后,通过
commit触发Mutation修改State,不允许直接修改State - 特点:可以是异步函数,支持批量触发多个
Mutation - 触发方式:组件中通过
this.$store.dispatch('action名', 载荷)或mapActions辅助函数触发
- 作用:用于执行异步操作(如接口请求、定时器),异步操作完成后,通过
示例:
export default new Vuex.Store({
mutations: { SET_USER_INFO(state, userInfo) { state.userInfo = userInfo } },
actions: {
// 异步获取用户信息
async getUserInfo({ commit }) {
const res = await Vue.axios.get('/api/user/info')
// 触发Mutation修改State
commit('SET_USER_INFO', res.data)
}
}
})
-
Module(模块拆分)
- 作用:将复杂的
Store按业务模块拆分(如用户模块、购物车模块、商品模块),每个模块拥有独立的State、Getter、Mutation、Action,提高代码可维护性 - 特点:默认情况下,模块内部的
Mutation和Action是全局的(可直接触发),State和Getter是局部的(需通过store.state.模块名访问);可通过namespaced: true开启命名空间,使模块内部的所有成员私有化
- 作用:将复杂的
示例:
// 模块1:user.js
const userModule = {
namespaced: true, // 开启命名空间
state: { userInfo: null },
mutations: { SET_USER_INFO(state, data) { state.userInfo = data } },
actions: { async getUserInfo({ commit }) { /* ... */ } }
}
// 模块2:cart.js
const cartModule = {
namespaced: true,
state: { cartList: [] },
mutations: { ADD_TO_CART(state, data) { /* ... */ } }
}
// store/index.js
export default new Vuex.Store({
modules: {
user: userModule,
cart: cartModule
}
})
// 组件中访问命名空间模块的状态
this.$store.state.user.userInfo
// 组件中触发命名空间模块的Action
this.$store.dispatch('user/getUserInfo')
2. Vuex中为什么必须通过Mutation修改State,而不能直接修改?
Vuex要求必须通过Mutation修改State,核心目的是保证状态变更的可追踪性、可预测性和可调试性,具体原因如下:
- 集中式状态变更管理:所有的状态修改都集中在
Mutation中,便于统一管理和维护,开发者可以快速定位到状态变更的逻辑,避免多个组件随意修改全局状态导致的混乱 - 支持DevTools调试:Vuex的DevTools可以记录每一次
Mutation的触发记录(包括Mutation名称、载荷、修改前后的State状态),支持时间旅行(回滚到任意历史状态),如果直接修改State,DevTools无法捕获到状态变更,失去调试能力 - 强制同步操作:
Mutation要求必须是同步函数,确保状态变更的同步性,避免异步操作导致的状态变更顺序混乱、不可预测(异步操作放在Action中执行,完成后再触发Mutation) - 便于实现中间件和插件:Vuex的中间件(如日志中间件、持久化插件)都是基于
Mutation的触发机制实现的,直接修改State会绕过这些中间件,导致插件失效
3. Vuex的持久化如何实现?
Vuex的State数据存储在内存中,页面刷新后会丢失,实现Vuex持久化的核心思路是将 State 数据同步存储到本地存储(localStorage/sessionStorage)中,页面刷新时从本地存储中读取数据并恢复到State中,常用实现方式有两种:
方式1:手动实现(简单场景)
通过Vuex的subscribe方法监听Mutation的触发,在Mutation执行后,将State数据存储到本地存储;在Store初始化时,从本地存储中读取数据并初始化State。
示例:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 从localStorage中读取持久化数据
const getLocalStorageState = () => {
const stateStr = localStorage.getItem('vuexState')
return stateStr ? JSON.parse(stateStr) : { userInfo: null, cartList: [] }
}
const store = new Vuex.Store({
state: getLocalStorageState(), // 初始化时读取本地存储
mutations: { /* ... */ },
actions: { /* ... */ }
})
// 监听Mutation,同步数据到localStorage
store.subscribe((mutation, state) => {
localStorage.setItem('vuexState', JSON.stringify(state))
})
export default store
方式2:使用第三方插件(推荐,复杂场景)
使用vuex-persistedstate插件,自动完成State数据与本地存储的同步,无需手动编写监听逻辑。
步骤:
-
安装插件
-
npm install vuex-persistedstate --save
-
-
配置插件
-
// store/index.js import Vue from 'vue' import Vuex from 'vuex' import createPersistedState from 'vuex-persistedstate' Vue.use(Vuex) export default new Vuex.Store({ state: { /* ... */ }, mutations: { /* ... */ }, actions: { /* ... */ }, plugins: [ createPersistedState({ // 配置项 storage: window.localStorage, // 存储方式(localStorage/sessionStorage) key: 'vuexState', // 本地存储的key名 paths: ['user', 'cart'] // 需要持久化的模块(默认持久化整个State) }) ] })
-
六、路由篇(Vue Router 3.x)
1. Vue Router 3.x的核心概念有哪些?各自的作用是什么?
Vue Router 3.x是Vue2官方配套的路由管理器,用于构建单页应用(SPA),核心概念如下:
1. Router(路由实例)
- 作用:创建路由实例,配置路由规则、全局守卫、模式等核心配置
- 创建方式:通过
new VueRouter({ ...config })创建,挂载到Vue实例上
示例:
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home'
Vue.use(Router)
export default new Router({
mode: 'history', // 路由模式(hash/history)
routes: [/* 路由规则 */]
})
2. Route(路由规则/当前路由信息)
-
作用:
routes配置中的单个路由规则(描述路径与组件的映射关系),或当前激活的路由信息对象 -
路由规则配置项:
path:路由路径(如/home、/user/:id(动态路由))name:路由名称(用于命名路由跳转,避免硬编码路径)component:路由对应的组件(如Home、() => import('@/views/User')(懒加载))props:是否将路由参数转为组件propsmeta:路由元信息(用于存储路由附加信息,如是否需要登录、页面标题)children:嵌套路由规则(用于页面内嵌套组件)
示例:
routes: [
{
path: '/',
name: 'Home',
component: Home,
meta: { requiresAuth: false, title: '首页' }
},
{
path: '/user/:id', // 动态路由
name: 'User',
component: () => import('@/views/User'), // 组件懒加载
props: true, // 路由参数转为props
meta: { requiresAuth: true, title: '用户中心' }
}
]
3. Router Link(路由链接)
- 作用:替代原生
<a>标签,实现无刷新的路由跳转,自动添加激活状态类名 - 常用属性:
to(跳转路径/路由对象)、name(路由名称)、params/query(传递参数)
示例:
<!-- 路径跳转 -->
<router-link to="/home">首页</router-link>
<!-- 命名路由跳转,传递参数 -->
<router-link :to="{ name: 'User', params: { id: 1 }, query: { tab: 'info' } }">
用户中心
</router-link>
4. Router View(路由出口)
-
作用:路由匹配到的组件将渲染在
router-view中,支持嵌套使用(对应嵌套路由) -
示例:
-
<!-- 根组件 App.vue --> <template> <div id="app"> <router-view></router-view> <!-- 根路由出口 --> </div> </template> <!-- 嵌套路由出口(Home组件) --> <template> <div class="home"> <h2>首页</h2> <router-view></router-view> <!-- 嵌套路由组件渲染位置 --> </div> </template>
-
5. 路由模式(mode)
hash模式(默认):路径以#开头(如http://localhost:8080/#/home),基于URL的hash值实现,无需后端配置,兼容性好history模式:路径无#(如http://localhost:8080/home),基于HTML5 History API实现,URL更美观,但需要后端配置(解决页面刷新404问题,配置所有请求转发到index.html)
2. 如何实现Vue Router的路由懒加载?有什么好处?
什么是路由懒加载
路由懒加载(也叫组件懒加载)是指在路由被访问时,才动态加载对应的组件文件,而不是在应用初始化时一次性加载所有组件,是Vue项目优化的重要手段。
实现方式(3种常用方式)
-
ES6的import()语法(推荐)
-
// router/index.js routes: [ { path: '/home', name: 'Home', // 路由被访问时,才加载Home组件 component: () => import('@/views/Home') }, { path: '/user', name: 'User', // 按需加载并指定chunk名称(打包时会生成单独的chunk文件,便于打包优化) component: () => import(/* webpackChunkName: "user" */ '@/views/User') } ]
-
-
Vue的异步组件(
Vue.component)-
import Vue from 'vue' routes: [ { path: '/about', name: 'About', component: Vue.component('About', () => import('@/views/About')) } ]
-
-
webpack的
require.ensure(旧版方式,不推荐)-
routes: [ { path: '/contact', name: 'Contact', component: resolve => require.ensure([], () => resolve(require('@/views/Contact')), 'contact') } ]
-
路由懒加载的好处
- 减小首屏加载体积:应用初始化时,只加载核心路由(如首页)的组件,不加载其他未访问的路由组件,减小首屏打包文件大小,提高首屏加载速度
- 优化用户体验:首屏加载更快,减少用户等待时间,避免白屏现象
- 按需加载资源:只有当用户访问对应路由时,才请求对应的组件资源,合理利用网络带宽,降低服务器初始请求压力
- 便于打包优化:通过指定
webpackChunkName,可以将多个路由组件打包到同一个chunk文件中,避免生成过多小文件,提高打包效率和资源加载效率
3. Vue Router的导航守卫有哪些?各自的作用和执行顺序是什么?
Vue Router的导航守卫用于拦截路由跳转过程,执行自定义逻辑(如登录验证、页面权限控制、页面标题修改) ,分为三大类:全局守卫、路由独享守卫、组件内守卫。
1. 全局守卫(全局生效,所有路由跳转都会触发)
-
router.beforeEach(全局前置守卫)
- 作用:路由跳转前拦截,最常用(如登录验证、权限判断)
- 执行时机:每次路由跳转前(在组件内守卫之前执行)
- 参数:
to(即将进入的路由对象)、from(即将离开的路由对象)、next(继续跳转的回调函数) - 注意:必须调用
next()才能继续跳转,next(false)取消跳转,next('/login')重定向到其他路由
示例:
// 登录验证
router.beforeEach((to, from, next) => {
// 判断路由是否需要登录
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
if (token) {
next() // 已登录,继续跳转
} else {
next('/login') // 未登录,重定向到登录页
}
} else {
next() // 无需登录,直接跳转
}
})
-
router.beforeResolve(全局解析守卫)
- 作用:与
beforeEach类似,但在所有组件内守卫和异步路由组件加载完成后执行 - 执行时机:
beforeEach之后,afterEach之前 - 适用场景:需要等待异步组件加载完成后执行的逻辑
- 作用:与
-
router.afterEach(全局后置守卫)
- 作用:路由跳转完成后执行,无
next参数(无法拦截跳转) - 执行时机:每次路由跳转完成后(最后执行)
- 适用场景:修改页面标题、统计页面访问量、隐藏加载动画
- 作用:路由跳转完成后执行,无
示例:
router.afterEach((to, from) => {
// 修改页面标题
document.title = to.meta.title || 'Vue2项目'
})
2. 路由独享守卫(单个路由生效)
-
beforeEnter
- 作用:仅对当前路由生效,拦截当前路由的跳转
- 配置位置:在路由规则中直接配置
- 执行时机:
beforeEach之后,组件内守卫之前 - 适用场景:单个路由的特殊权限控制
示例:
routes: [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin'),
meta: { requiresAuth: true },
// 路由独享守卫
beforeEnter: (to, from, next) => {
const isAdmin = localStorage.getItem('isAdmin')
if (isAdmin) {
next()
} else {
next('/403') // 无权限,重定向到403页面
}
}
}
]
3. 组件内守卫(组件内部生效)
-
beforeRouteEnter(组件进入前)
- 作用:组件被路由渲染前拦截
- 执行时机:
beforeEnter之后,mounted之前 - 注意:此时组件实例尚未创建(
this为undefined),如需访问组件实例,可通过next(vm => { /* 操作vm(组件实例) */ }) - 适用场景:组件进入前的初始化逻辑、获取组件所需数据
-
beforeRouteUpdate(组件更新时)
- 作用:路由参数变化但组件复用(如
/user/1→/user/2)时触发 - 执行时机:路由参数变化后,组件更新前
- 注意:此时组件实例已存在(
this可访问) - 适用场景:路由参数变化后,更新组件数据
- 作用:路由参数变化但组件复用(如
示例:
export default {
beforeRouteUpdate(to, from, next) {
// 路由参数id变化,重新获取用户信息
this.userId = to.params.id
this.getUserInfo()
next()
}
}
-
beforeRouteLeave(组件离开前)
- 作用:组件被路由离开前拦截
- 执行时机:组件离开前,
beforeEach之前(针对下一个路由) - 注意:此时组件实例已存在(
this可访问) - 适用场景:提示用户保存未提交的表单、取消定时器
示例:
export default {
beforeRouteLeave(to, from, next) {
if (this.formIsModified) {
const confirm = window.confirm('表单内容未保存,是否确认离开?')
if (confirm) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
导航守卫执行顺序(完整路由跳转流程)
- 触发路由跳转(如
router.push、router-link) - 全局前置守卫:
router.beforeEach - 组件内守卫(离开当前组件):
beforeRouteLeave - 路由独享守卫:
beforeEnter - 解析异步路由组件(如有)
- 组件内守卫(进入目标组件):
beforeRouteEnter - 全局解析守卫:
router.beforeResolve - 导航完成,更新DOM(渲染目标组件)
- 全局后置守卫:
router.afterEach - 组件内守卫(目标组件):
beforeRouteEnter的next(vm => {})回调(组件实例创建后)
七、性能优化篇
1. Vue2项目有哪些常见的性能优化手段?
Vue2项目的性能优化分为开发阶段优化、打包阶段优化、运行阶段优化三大类,覆盖项目全生命周期,具体手段如下:
一、开发阶段优化(编码层面)
-
合理使用v-if和v-show
- 频繁切换显示/隐藏:使用
v-show(修改CSS,性能更高) - 条件不常变化或需要销毁组件:使用
v-if(创建/销毁DOM,节省内存)
- 频繁切换显示/隐藏:使用
-
避免v-for和v-if同时使用
- 优先使用计算属性过滤数据,再进行
v-for遍历(详见核心语法篇)
- 优先使用计算属性过滤数据,再进行
-
为v-for设置唯一key
- 使用唯一且稳定的
key(如后端返回的id,不建议使用index),帮助Vue的diff算法高效对比虚拟DOM,减少不必要的DOM操作
- 使用唯一且稳定的
-
合理使用计算属性和侦听器
- 复杂数据推导使用
computed(利用缓存,避免重复计算) - 异步操作和复杂业务逻辑使用
watch(避免在computed中执行异步操作)
- 复杂数据推导使用
-
组件懒加载(路由懒加载+组件按需加载)
- 路由懒加载:减少首屏打包体积(详见路由篇)
- 组件按需加载:非核心组件(如弹窗、图表)采用懒加载,避免初始化时加载
-
减少不必要的DOM操作
- 利用Vue的数据驱动视图,避免手动操作
DOM - 复杂DOM结构使用虚拟列表(如
vue-virtual-scroller),只渲染可视区域内的DOM元素(适用于长列表)
- 利用Vue的数据驱动视图,避免手动操作
-
避免不必要的响应式数据
- 对于不需要响应式的数据(如静态配置、大型第三方数据),直接挂载到
this上(如this.config = { ... }),而非放入data中(避免Object.defineProperty()劫持,节省性能)
- 对于不需要响应式的数据(如静态配置、大型第三方数据),直接挂载到
-
合理使用插槽和组件复用
- 提取公共组件(如按钮、输入框、卡片),提高复用性,减少冗余代码
- 复杂组件使用插槽分发内容,提高灵活性,避免组件过于臃肿
二、打包阶段优化(webpack配置层面)
-
优化webpack配置
- 开启
productionSourceMap: false(生产环境不生成sourceMap,减小打包体积) - 配置
chainWebpack或configureWebpack,拆分chunk(如将第三方库打包为单独chunk) - 开启代码压缩(
terser-webpack-plugin压缩JS,css-minimizer-webpack-plugin压缩CSS)
- 开启
-
CDN引入第三方库
- 将大型第三方库(如Vue、Vuex、Vue Router、Element UI)通过CDN引入,不打包到项目中,减小打包体积,提高加载速度
- 配置:
vue.config.js中externals排除第三方库,public/index.html中引入CDN链接
示例:
// vue.config.js
module.exports = {
configureWebpack: {
externals: {
vue: 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT'
}
}
}
-
图片资源优化
- 小图片转为base64(
url-loader配置limit),减少HTTP请求 - 大图片使用WebP格式,或采用图片懒加载(
vue-lazyload) - 使用CDN存储图片资源,提高图片加载速度
- 小图片转为base64(
-
开启Gzip压缩
- 配置
compression-webpack-plugin,打包生成Gzip压缩文件 - 后端配置(Nginx/Apache)支持Gzip,减少文件传输体积
- 配置
三、运行阶段优化(项目上线后)
-
利用浏览器缓存
- 配置静态资源的缓存策略(Nginx配置
Cache-Control、ETag),长期缓存不变的静态资源(如JS、CSS、图片) - 打包时为静态资源添加哈希值(如
app.[hash].js),确保资源更新时浏览器能获取最新版本
- 配置静态资源的缓存策略(Nginx配置
-
服务端渲染(SSR)/静态站点生成(SSG)
- 对于SEO要求高、首屏加载速度要求高的项目,采用Vue SSR(
nuxt.js),在服务端渲染页面,返回完整的HTML给浏览器,提高首屏加载速度和SEO效果
- 对于SEO要求高、首屏加载速度要求高的项目,采用Vue SSR(
-
监控性能瓶颈
- 使用Vue DevTools的Performance面板,分析组件渲染和更新性能
- 使用浏览器DevTools的Performance面板,分析页面加载、脚本执行、DOM渲染的性能瓶颈
- 接入前端性能监控平台(如Sentry、Fundebug),实时监控线上项目的性能问题和报错
2. 如何解决Vue2中长列表的性能问题?
长列表(如1000+条数据)直接渲染会导致DOM元素过多、虚拟DOM diff算法耗时过长、页面卡顿、首屏加载缓慢等性能问题,解决方案如下:
方案1:虚拟列表(推荐,适用于超长长列表)
虚拟列表的核心思想是只渲染可视区域内的DOM元素,滚动时动态替换可视区域内的数据和DOM元素,不渲染不可见区域的内容,从而减少DOM数量,提高渲染性能和滚动流畅度。
常用的Vue2虚拟列表插件:
vue-virtual-scroller(轻量、易用,支持无限滚动)vue-virtual-list(简单高效,适用于固定高度列表)
使用示例(vue-virtual-scroller):
-
安装插件
-
npm install vue-virtual-scroller --save
-
-
全局注册
-
// main.js import Vue from 'vue' import VueVirtualScroller from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' Vue.use(VueVirtualScroller)
-
-
组件中使用
-
<template> <div class="long-list"> <RecycleScroller :items="longList" :item-size="80" // 每个列表项的高度(固定高度) key-field="id" // 列表项的唯一key class="scroller" > <template v-slot="{ item }"> <!-- 列表项内容 --> <div class="list-item"> <h3>{{ item.name }}</h3> <p>{{ item.desc }}</p> </div> </template> </RecycleScroller> </div> </template> <script> export default { data() { return { longList: [] // 超长列表数据(10000条+) } }, mounted() { // 模拟获取超长列表数据 for (let i = 0; i < 10000; i++) { this.longList.push({ id: i, name: `列表项${i + 1}`, desc: `这是第${i + 1}条列表数据` }) } } } </script> <style scoped> .long-list { height: 600px; overflow: auto; } .scroller { height: 100%; } .list-item { height: 80px; padding: 10px; border-bottom: 1px solid #eee; } </style>
-
方案2:分页加载(适用于可分页的长列表)
分页加载的核心思想是将长列表数据按页码拆分,每次只渲染一页数据(如10/20条),用户点击页码或滚动到底部时加载下一页数据,减少单次渲染的DOM数量。
实现方式:
- 后端支持分页接口(返回当前页数据和总页数)
- 前端维护
pageNum(当前页码)、pageSize(每页条数)、total(总条数) - 滚动到底部时(或点击下一页),请求下一页数据,追加到列表中
示例(滚动到底部加载下一页):
<template>
<div class="long-list" @scroll="handleScroll">
<div class="list-item" v-for="item in list" :key="item.id">
<h3>{{ item.name }}</h3>
<p>{{ item.desc }}</p>
</div>
<div v-if="loading">加载中...</div>
<div v-if="noMore">没有更多数据了</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [],
pageNum: 1,
pageSize: 20,
total: 0,
loading: false,
noMore: false
}
},
mounted() {
this.getListData()
},
methods: {
// 获取列表数据
async getListData() {
this.loading = true
const res = await this.$axios.get('/api/list', {
params: { pageNum: this.pageNum, pageSize: this.pageSize }
})
this.list = [...this.list, ...res.data.list]
this.total = res.data.total
this.loading = false
// 判断是否有更多数据
this.noMore = this.list.length >= this.total
},
// 滚动到底部加载下一页
handleScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.target
// 滚动到底部(距离底部20px时触发)
if (scrollTop + clientHeight >= scrollHeight - 20 && !this.loading && !this.noMore) {
this.pageNum++
this.getListData()
}
}
}
}
</script>
方案3:数据懒加载+防抖(适用于中等长度列表)
对于中等长度列表(如500条以内),可采用数据懒加载+防抖的方式,减少一次性渲染的压力:
- 初始化时只渲染部分数据(如前50条)
- 监听滚动事件,滚动到底部时,追加渲染下一部分数据(如再渲染50条)
- 对滚动事件添加防抖处理,避免频繁触发渲染逻辑
八、常见问题与排错篇
1. Vue2中修改了data中的数据,视图为什么没有更新?
这是Vue2开发中高频问题,核心原因是数据修改未被Vue的响应式系统检测到,具体场景及解决方案如下:
场景1:修改了对象新增的属性(未被Object.defineProperty劫持)
// 初始化data
data() {
return {
user: { name: '张三' }
}
}
// 错误修改:新增age属性,视图不更新
this.user.age = 25
// 解决方案
this.$set(this.user, 'age', 25) // 方式1:使用$set
this.user = { ...this.user, age: 25 } // 方式2:重新赋值对象
场景2:修改了数组的下标或长度(Vue未封装该操作)
// 初始化data
data() {
return {
arr: [1, 2, 3]
}
}
// 错误修改:下标修改/长度修改,视图不更新
this.arr[0] = 10
this.arr.length = 0
// 解决方案
this.$set(this.arr, 0, 10) // 方式1:$set修改下标
this.arr.splice(0, 1, 10) // 方式2:使用Vue封装的数组方法
this.arr = [...this.arr.slice(0, 0), 10, ...this.arr.slice(1)] // 方式3:重新赋值数组
场景3:修改了深层嵌套对象的属性(未触发setter)
// 初始化data
data() {
return {
obj: { a: { b: { c: 1 } } }
}
}
// 错误修改:深层嵌套属性,视图可能不更新(尤其是多层嵌套)
this.obj.a.b.c = 2
// 解决方案
this.$set(this.obj.a.b, 'c', 2) // 方式1:$set修改深层属性
this.obj = JSON.parse(JSON.stringify(this.obj)) // 方式2:深拷贝后重新赋值(简单粗暴,不推荐复杂对象)
this.obj.a.b = { ...this.obj.a.b, c: 2 } // 方式3:逐层重新赋值(推荐)
场景4:异步操作中修改数据,未正确触发更新
// 错误示例:定时器中修改数据,视图可能不更新(罕见,Vue通常能检测到)
setTimeout(() => {
this.message = '异步修改数据'
}, 1000)
// 解决方案(若出现不更新):强制触发更新
setTimeout(() => {
this.message = '异步修改数据'
this.$forceUpdate() // 强制组件重新渲染(不推荐频繁使用,会跳过响应式系统)
}, 1000)
场景5:数据被冻结(Object.freeze())
// 错误示例:冻结对象后,修改数据不更新
data() {
return {
obj: Object.freeze({ name: '张三' })
}
}
this.obj.name = '李四' // 视图不更新
// 解决方案:不要冻结需要响应式更新的对象,或重新赋值一个未冻结的对象
this.obj = { ...this.obj, name: '李四' }
2. Vue2中如何解决跨域问题?
跨域是指浏览器的同源策略限制(协议、域名、端口任一不同即为跨域),Vue2项目中解决跨域的方式主要有以下3种:
方式1:开发环境(本地调试)- 配置Vue CLI代理(推荐)
在vue.config.js中配置devServer.proxy,通过本地开发服务器转发请求,避免浏览器跨域限制(仅适用于开发环境)。
示例:
// vue.config.js
module.exports = {
devServer: {
proxy: {
// 匹配以/api开头的请求
'/api': {
target: 'https://api.example.com', // 目标后端接口地址
changeOrigin: true, // 开启跨域模拟(修改请求头的Host为目标地址)
pathRewrite: { '^/api': '' } // 路径重写(去掉请求路径中的/api)
}
}
}
}
使用:
// 前端请求(无需写完整地址,直接写/api开头)
this.$axios.get('/api/user/info').then(res => { /* ... */ })
// 实际转发后的请求地址:https://api.example.com/user/info
方式2:生产环境 - 后端配置CORS(跨域资源共享)
由后端在响应头中添加允许跨域的配置,允许前端域名访问,常用响应头如下:
Access-Control-Allow-Origin: https://www.frontend.com // 允许的前端域名(*表示允许所有域名)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的请求方法
Access-Control-Allow-Headers: Content-Type, Authorization // 允许的请求头
Access-Control-Allow-Credentials: true // 允许携带Cookie(需前后端同时配置)
方式3:生产环境 - 反向代理(Nginx/Apache)
通过Nginx配置反向代理,将前端的请求转发到后端接口,避免浏览器跨域(生产环境常用方案)。
Nginx配置示例:
# nginx.conf
server {
listen 80;
server_name www.frontend.com; // 前端域名
# 转发/api开头的请求到后端接口
location /api/ {
proxy_pass https://api.example.com/; // 后端接口地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 前端静态资源配置
location / {
root /path/to/frontend/dist; // 前端打包后的dist目录
index index.html;
try_files $uri $uri/ /index.html; // 解决SPA路由刷新404问题
}
}
3. Vue2中如何解决组件之间的样式冲突?
Vue2组件中的样式默认是全局生效的,多个组件使用相同的类名会导致样式冲突,解决方案如下:
方式1:使用scoped属性(推荐,组件内样式隔离)
在组件的<style>标签上添加scoped属性,Vue会为当前组件的所有DOM元素添加一个唯一的data-v-xxx属性,并为样式添加对应的属性选择器,使样式仅对当前组件生效,不会污染其他组件。
示例:
<template>
<div class="container">组件内容</div>
</template>
<!-- scoped样式,仅当前组件生效 -->
<style scoped>
.container {
width: 100%;
padding: 20px;
}
</style>
注意事项:
scoped样式不会影响子组件的根元素(便于父组件控制子组件根元素样式)- 如需修改子组件内部样式,可使用深度选择器(
/deep/或::v-deep,Vue2中推荐/deep/)
示例:
<style scoped>
/* 深度选择器:修改子组件内部样式 */
.parent-container /deep/ .child-item {
color: red;
}
</style>
方式2:使用CSS Modules(模块化CSS)
CSS Modules将CSS样式模块化,每个类名会被编译为唯一的哈希值,避免样式冲突,适用于复杂项目。
使用示例:
<template>
<!-- 绑定模块化样式 -->
<div :class="$style.container">组件内容</div>
</template>
<!-- 开启CSS Modules -->
<style module>
.container {
width: 100%;
padding: 20px;
}
</style>
方式3:使用BEM命名规范(手动避免冲突)
BEM(Block-Element-Modifier)是一种CSS命名规范,通过统一的命名格式避免类名冲突,适用于不使用scoped和CSS Modules的场景。
命名格式:块名__元素名--修饰符
- 块名:组件名称(如
user-card) - 元素名:组件内的元素(如
user-card__avatar) - 修饰符:元素的状态(如
user-card__avatar--large)
示例:
<template>
<div class="user-card">
<img class="user-card__avatar user-card__avatar--large" src="" alt="">
<div class="user-card__name"></div>
</div>
</template>
<style>
.user-card {
width: 300px;
border: 1px solid #eee;
}
.user-card__avatar {
width: 80px;
height: 80px;
}
.user-card__avatar--large {
width: 100px;
height: 100px;
}
</style>