一、Vue的生命周期
Vue2的生命周期:
问题点:
- 什么场景下会在mounted里调用接口,而不是created
- 为什么不在beforeMount调用接口?
- 答:与created没啥区别,那为什么不提前调用呢,减少白屏
Vue3的生命周期:
| Vue 2 | Vue 3 (选项式) | Vue 3 (组合式) | 说明 |
|---|---|---|---|
| beforeCreate | ✅ beforeCreate | ❌ 无(直接用setup) | 实例初始化前 |
| created | ✅ created | ❌ 无(直接用setup) | 实例创建后 |
| beforeMount | ✅ beforeMount | onBeforeMount | 挂载前 |
| mounted | ✅ mounted | onMounted | 挂载后 |
| beforeUpdate | ✅ beforeUpdate | onBeforeUpdate | 更新前 |
| updated | ✅ updated | onUpdated | 更新后 |
| beforeDestroy | ➡️ beforeUnmount | onBeforeUnmount | 卸载前 |
| destroyed | ➡️ unmounted | onUnmounted | 卸载后 |
| - | ✅ errorCaptured | onErrorCaptured | 捕获错误 |
| - | ✅ renderTracked | onRenderTracked | 调试:追踪依赖 |
| - | ✅ renderTriggered | onRenderTriggered | 调试:触发重绘 |
| activated | ✅ activated | onActivated | keep-alive激活 |
| deactivated | ✅ deactivated | onDeactivated | keep-alive停用 |
问题点:
-
setup替代了beforeCreate和created生命周期
-
onErrorCaptured用途和能捕获哪些错误:
用途 说明 错误上报 将错误发送到监控系统(如 Sentry) 降级 UI 显示错误提示,防止白屏 错误隔离 某个组件出错不影响其他部分 日志记录 记录用户操作路径,便于复现 阻止错误传播 控制错误是否继续向上传递 ✅ 能捕获的错误类型
错误类型 能否捕获 示例 组件渲染错误 ✅ 能 模板中调用不存在的方法 生命周期钩子错误 ✅ 能 mounted、created中的错误事件处理错误 ✅ 能 @click绑定的方法中抛错计算属性错误 ✅ 能 计算属性中抛出异常 watch 回调错误 ✅ 能 监听的回调函数中抛错 异步组件加载错误 ✅ 能 动态 import()失败setup 函数错误 ✅ 能 组合式 API 中初始化错误 ❌ 不能捕获的错误类型
错误类型 能否捕获 原因 异步任务中的错误 ❌ 不能 setTimeout、Promise内的错误(需自行 catch)自身组件的错误 ❌ 不能 只能捕获后代组件,不能捕获自己 事件总线错误 ❌ 不能 非组件树的全局事件 DOM 事件错误 ❌ 不能 addEventListener绑定的原生事件语法错误 ❌ 不能 如 console.log(a.b)(a 未定义)在父组件
二、Vue的路由生命周期
// 全局前置守卫 - 登录验证
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLogin) {
next('/login')
} else {
next()
}
})
// 组件内守卫 - 离开确认
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedData) {
const confirm = window.confirm('有未保存内容,确定离开?')
confirm ? next() : next(false)
} else {
next()
}
}
三、watch和computed有什么差异化
watch
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
deep | boolean | false | 是否深度监听(对象内部变化) |
immediate | boolean | false | 是否在创建时立即执行一次 |
flush | string | 'pre' | 回调执行时机:'pre'(组件更新前)、'post'(组件更新后)、'sync'(同步) |
once (Vue 3.4+) | boolean | false | 是否只触发一次 |
// 基础监听
watch: {
// 简单监听
count(newVal, oldVal) {
console.log(`count从${oldVal}变成${newVal}`)
},
// 深度监听 + 立即执行
'$route.params.id': {
handler(id) {
// 组件首次加载时获取数据
// 从 /user/1 切换到 /user/2 时也获取数据
this.getUserDetail(id)
},
deep: true, // 监听对象内部变化
immediate: true // 立即执行
}
}
// Vue 3 组合式
watch(user, (newVal, oldVal) => {
console.log('user变化了')
}, { deep: true, immediate: true })
watch和computed差异化
| 对比项 | Computed (计算属性) | Watch (侦听器) |
|---|---|---|
| 核心作用 | 计算并返回新数据 | 监听变化执行操作 |
| 缓存机制 | ✅ 有缓存 | ❌ 无缓存 |
| 使用场景 | 数据格式化、组合、计算 | 异步请求、DOM操作、复杂逻辑 |
| 返回值 | 必须返回一个值 | 一般不返回值 |
| 异步支持 | ❌ 不支持 | ✅ 支持 |
| 初始化执行 | 自动执行一次 | 需加 immediate: true |
| 代码风格 | 声明式(简洁) | 命令式(灵活) |
总结:
- computed:根据依赖项返回一个新的值(有缓存)
- watch:根据依赖项执行其他操作
四、data什么场景下是函数
- 组件内:组件会多次复用,防止数据污染,所以是函数(返回一个新的对象)
- 根实例:不会复用
五、插槽
子组件:slotChild1.vue
<template>
<div class="container">
<slot></slot>
</div>
</template>
父组件:
<div class="content">
<span class="contentTitle">默认插槽</span>
<slotChild1>
<span>插槽内文本</span>
</slotChild1>
</div>
子组件:slotChild2.vue
<template>
<div class="container">
<slot name="one"></slot>
<slot name="two"></slot>
</div>
</template>
父组件:
<div class="content">
<span class="contentTitle">具名插槽</span>
<slotChild2>
<!-- 具名插槽定义name,能找到指定的插槽,插入标签 -->
<template v-slot:one>
<div style="margin-bottom:10px">插槽内文本1</div>
</template>
<template v-slot:two>
<div style="margin-bottom:10px">插槽内文本2</div>
</template>
</slotChild2>
</div>
子组件:slotChild3.vue
<template>
<div class="container">
<slot :sendDataByChildC='"我是子组件的数据"'></slot>
</div>
</template>
父组件:
<div class="content">
<span class="contentTitle">作用域插槽</span>
<slotChild3>
<!-- 作用域插槽,可通过子组件插槽传递给父组件数据 -->
<template slot-scope="data">
<div style="margin-top:10px">{{data.sendDataByChildC||'空值'}}</div>
</template>
</slotChild3>
</div>
总结:
- 插槽:父组件决定内容
- 子组件:子组件决定内容
五、Vuex/Pinia(存储数据)
Vuex
📁 store/modules/router.js
const state= {
routerInfo:[]
}
const mutations = {
SET_ROUTERINFO: (state, routerInfo) => {
state.routerInfo = routerInfo
}
}
const actions = {
getRouters({
commit,
state
}) {
return new Promise((resolve, reject) => {
getRouters().then(response => {
commit('SET_ROUTERINFO', response.data)
resolve(response.data)
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true, // 解决命名冲突
state,
mutations,
actions
}
📁 store/getters.js
const getters = {
routerInfo: state => state.user.routerInfo
}
export default getters
📁 store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import router from './modules/router'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
router
},
getters
})
export default store
组件内使用
<script>
import store from '@/store'
export default {
mounted(){
方式1:
store.dispatch('router/getRouters').then(res=>{
console.log('获取 路由信息',res)
})
方式2:
this.getRouters().then(res => {
console.log('获取路由信息', res)
// 处理返回的数据
})
},
methods:{
方式2:
...mapActions('user', ['getRouters']) // 模块->方法
}
}
</script>
Pinia
// 一个简单的 Pinia Store 示例
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
// state
state: () => ({
name: '张三',
age: 25,
token: ''
}),
// getters
getters: {
doubleAge: (state) => state.age * 2,
isAdult: (state) => state.age >= 18
},
// actions
actions: {
updateName(newName) {
this.name = newName // 直接修改!
},
async login(credentials) {
const res = await api.login(credentials)
this.token = res.token // 直接修改!
}
}
})
使用:
<template>
<p>姓名:{{ userStore.name }}</p>
<div>
<h3>直接修改 state</h3>
<button @click="userStore.name = '李四'">修改姓名为李四</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
// 获取 store 实例
const userStore = useUserStore()
// 调用action的方法
const handleLogin = async () => {
// 模拟登录
await userStore.login({
username: 'test',
password: '123'
})
console.log('登录成功,token:', userStore.token)
}
// 批量修改
const batchUpdate = () => {
// 使用 $patch 一次修改多个状态
userStore.$patch({
name: '赵六',
age: 28,
token: 'xyz789'
})
}
// 重置store到初始状态
const resetStore = () => {
userStore.$reset()
}
</script>
六、Mixin 混入
// mixins/booleanMixin.js
export default {
data(){
return {
valueIsUpdate:false
}
},
watch:{
valueIsUpdate(value){
console.log('mixin---',value)
}
}
}
// .vue
<template>
<div class="container">
<el-button @click="valueIsUpdate=true">试一下</el-button>
<el-button @click="valueIsUpdate=false">试一下2</el-button>
</div>
</template>
<script>
import booleanMixin from "@/mixins/booleanMixin"
export default {
mixins:[booleanMixin]
}
</script>
重点:(v2、v3用法一致,但v3不推荐)
v3替代用法:(Hooks:组合式函数)
// composables/useBoolean.js
import { ref, watch } from 'vue'
export function useBoolean() {
// 一比一还原 data 中的数据
const valueIsUpdate = ref(false)
// 一比一还原 watch 监听
watch(valueIsUpdate, (value) => {
console.log('mixin---', value)
})
// 返回所有需要暴露的属性
return {
valueIsUpdate
}
}
// .vue
<template>
<div class="container">
<el-button @click="valueIsUpdate = true">点一下</el-button>
<el-button @click="valueIsUpdate = false">点一下2</el-button>
</div>
</template>
<script setup>
import { useBoolean } from "@/composables/useBoolean"
// 一比一还原,直接解构出 valueIsUpdate
const { valueIsUpdate } = useBoolean()
</script>
七、传值
1、父传子 (Props)
<!-- 父组件 Parent.vue -->
<template>
<div>
<Child :message="'parent msg'" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child }
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message'] // 接收父组件传值
}
</script>
2、 子传父 ($emit)
<!-- 子组件 Child.vue -->
<template>
<button @click="sendData">发送数据给父组件</button>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('child-event', 'Hello Parent!')
}
}
}
</script>
<!-- 父组件 Parent.vue -->
<template>
<div>
<Child @child-event="handleChildData" />
<p>收到子组件数据: {{ childData }}</p>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
childData: ''
}
},
methods: {
handleChildData(data) {
this.childData = data
}
}
}
</script>
3、兄弟组件传值 (Event Bus)
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
<!-- 兄弟组件A BrotherA.vue -->
<template>
<button @click="sendMsg">发送消息给兄弟</button>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
methods: {
sendMsg() {
EventBus.$emit('msg-to-b', 'Hello Brother B!')
}
}
}
</script>
<!-- 兄弟组件B BrotherB.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
message: ''
}
},
mounted() {
EventBus.$on('msg-to-b', (data) => {
this.message = data
})
},
beforeDestroy() {
EventBus.$off('msg-to-b') // 记得销毁监听
}
}
</script>
4、祖孙组件传值 (Provide / Inject)
<!-- 祖先组件 Grandparent.vue -->
<template>
<div>
<Parent />
</div>
</template>
<script>
import Parent from './Parent.vue'
export default {
components: { Parent },
provide() {
return {
grandparentMsg: this.message
}
},
data() {
return {
message: 'Hello from Grandparent!'
}
}
}
</script>
<!-- 孙组件 Grandchild.vue -->
<template>
<div>{{ grandparentMsg }}</div>
</template>
<script>
export default {
inject: ['grandparentMsg'] // 直接接收祖先数据
}
</script>
5、Vuex 状态管理(同上)
6、parent / $children
<!-- 父组件 -->
<template>
<div>
<Child ref="childComp" />
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
methods: {
callChildMethod() {
this.$refs.childComp.childMethod() // 调用子组件方法
console.log(this.$children) // 所有子组件实例
}
}
}
</script>
vue3差异化:
- 子组件接收props
<script setup>
const props = defineProps({
message: String,
count: Number
})
// 或者使用数组语法
// const props = defineProps(['message', 'count'])
</script>
2. .sync(父子组件双向绑定语法糖) vue2写法:
<template>
<!-- 使用 .sync 修饰符 -->
<Child :title.sync="title" />
<!-- 等价于 -->
<Child :title="title" @update:title="newTitle => title = newTitle" />
</template>
<script>
export default {
data() {
return {
title: 'Hello'
}
}
}
</script>
// 子组件
<template>
<div>
<h1>{{ title }}</h1>
<button @click="updateTitle">修改标题</button>
</div>
</template>
<script>
export default {
props: ['title'],
methods: {
updateTitle() {
// 子组件通过触发 update:title 事件来修改父组件的 title
this.$emit('update:title', '新标题')
}
}
}
</script>
vue3写法:
// 父组件
<template>
<!-- Vue 3 使用 v-model:propName -->
<Child v-model:title="title" />
</template>
<script setup>
import { ref } from 'vue'
const title = ref('Hello')
</script>
// 子组件
<template>
<div>
<h1>{{ title }}</h1>
<button @click="updateTitle">修改标题</button>
</div>
</template>
<script setup>
const props = defineProps(['title'])
const emit = defineEmits(['update:title'])
const updateTitle = () => {
// 子组件通过触发 update:title 事件来修改父组件的 title
emit('update:title', '新标题')
}
</script>
八、页面刷新后,vuex数据丢失,处理方式有
1、localStorage/sessionStorage
2、vuex-persistedstate 插件
npm install vuex-persistedstate
// store/index.js
import createPersistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
state: {
userInfo: null,
token: '',
permissions: [],
// 不想持久化的数据
tempData: null
},
mutations: { /* ... */ },
plugins: [
createPersistedState({
// 存储方式,默认 localStorage
storage: window.sessionStorage,
// 只持久化指定模块或字段
reducer: (state) => ({
userInfo: state.userInfo,
token: state.token,
permissions: state.permissions
}),
// 自定义 key
key: 'my-app-vuex'
})
]
})
3、路由守卫中重新获取数据
// router/index.js
router.beforeEach(async (to, from, next) => {
const token = store.state.token
if (token) {
// 有 token 但用户信息缺失,重新获取
if (!store.state.userInfo) {
try {
await store.dispatch('getUserInfo')
} catch (error) {
// token 失效,跳转登录
store.dispatch('logout')
next('/login')
return
}
}
next()
} else {
// 未登录,白名单页面放行
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
})
4、等等
九、页面刷新后,出现白屏,如何优化
1、路由懒加载
// router/index.js
const routes = [
{
path: '/',
name: 'Home',
// 路由懒加载,按需加载组件
component: () => import('@/views/Home.vue')
}
]
2、添加加载骨架屏
3、资源加载优化
// vite.config.js (Vite 项目)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// 1、代码分割 (减少重复加载,从缓存拿数据)
rollupOptions: {
output: {
manualChunks: {
// 将第三方库单独打包
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus', 'ant-design-vue']
}
}
},
// 2、压缩代码
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true
}
},
// 3、生成 source map(生产环境建议关闭)
sourcemap: false,
// 4、chunk 大小警告限制
chunkSizeWarningLimit: 1000
}
})
4、添加全局错误处理
5、添加加载进度条
6、首屏加载优化
使用 CDN 加速
十、首页加载慢,如何优化
1、路由懒加载
2、第三方库按需引入+CDN外置
将固定依赖-vue/router/ui从打包中剔除,CDN加载
3、雪碧图/图片懒加载/图片压缩
4、优化白屏时间
5、代码优化
十一、vue有哪些常见指令
- v-bind(动态属性 :html)
- v-model
- v-if/v-else/v-else-if/v-show
- v-for
- v-on(简写@:@click="aa")
- v-html(渲染html字符串)
- v-text(等同于插值方式{{}})
- v-pre(跳过编译,显示原始内容)
- v-once(只渲染一次,后续数据变化不会更新)
- v-memo(vue3.2+,缓存子树,用于性能优化)
<div v-memo="[user.id]">
<!-- 只有 user.id 变化时才重新渲染 -->
</div>
十二、如何自定义指令(防抖/节流)
防抖:多次点击,只执行最后一次(搜索输入,窗口resize)
节流:多次点击,每隔多少秒执行一次(滚动加载/按钮点击)
// main.js
import debounce from "@/utils/debounce"
import throttle from "@/utils/throttle"
// utils/debounce.js
// utils/debounce.js 防抖
import Vue from 'vue'
// 防抖:延迟执行,多次触发只执行最后一次
Vue.directive('debounce', {
// 当指令绑定到元素时
bind(el, binding) {
// 确保传入的是函数
if (typeof binding.value !== 'function') {
throw new Error('v-debounce 需要传入函数');
}
let timer = null; // 定时器ID
// 防抖处理函数
const debounceHandler = (...args) => {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
// 延迟执行原函数
binding.value.apply(this, args);
}, binding.arg || 500); // 延迟时间可从参数获取,默认500ms
};
// 保存原始事件处理函数,方便解绑
el._debounceHandler = debounceHandler;
// 绑定事件
el.addEventListener('click', debounceHandler);
},
// 指令解绑时清理
unbind(el) {
if (el._debounceHandler && el._debounceHandler.timer) {
clearTimeout(el._debounceHandler.timer);
}
el.removeEventListener('click', el._debounceHandler);
delete el._debounceHandler;
}
});
// utils/throttle.js 节流
import Vue from 'vue'
// 节流:固定时间内只执行一次,稀释执行频率
Vue.directive('throttle', {
bind(el, binding) {
if (typeof binding.value !== 'function') {
throw new Error('v-throttle 需要传入函数');
}
let canRun = true;
let timer = null;
// 节流处理函数
const throttleHandler = (...args) => {
if (!canRun) return;
canRun = false;
binding.value.apply(this, args);
// 清除之前的定时器,避免累积
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
canRun = true;
timer = null;
}, binding.arg || 300);
};
// 保存到元素上,方便清理
el._throttleHandler = throttleHandler;
el._throttleTimer = () => timer; // 保存定时器引用
el.addEventListener('click', throttleHandler);
},
unbind(el) {0
// 清理定时器
if (el._throttleTimer) {
clearTimeout(el._throttleTimer());
}
// 移除事件监听
el.removeEventListener('click', el._throttleHandler);
// 清理属性
delete el._throttleHandler;
delete el._throttleTimer;
}
});
// .vue
<template>
<div>
<!-- 防抖:快速点击多次,只执行最后一次,延迟1秒后触发 -->
<button v-debounce:1000="handleDebounce">
防抖按钮
</button>
<!-- 节流:快速点击多次,每300ms最多执行一次 -->
<button v-throttle:2000="handleThrottle">
节流按钮
</button>
</div>
</template>
<script>
export default {
methods: {
// 防抖处理函数
handleDebounce() {
console.log('防抖执行', new Date().toLocaleTimeString());
},
// 节流处理函数
handleThrottle() {
console.log('节流执行', new Date().toLocaleTimeString());
}
}
};
</script>
十三、listeners
- $attrs
<!-- 父组件 -->
<template>
<CustomInput
type="text"
placeholder="请输入"
class="my-input"
data-testid="input-1"
@focus="handleFocus"
/>
</template>
<!-- 子组件 CustomInput.vue -->
<template>
<div class="custom-input-wrapper">
<!-- 透传所有未声明的属性到 input 元素 -->
<input v-bind="$attrs" v-on="$listeners" />
</div>
</template>
- $listeners
<!-- 父组件 -->
<template>
<CustomButton
@click="handleClick"
@focus="handleFocus"
@custom-event="handleCustom"
/>
</template>
<!-- 子组件 CustomButton.vue -->
<template>
<button
class="custom-button"
v-bind="$attrs"
v-on="$listeners" <!-- 透传所有事件 -->
>
<slot />
</button>
</template>