Vue基础知识巩固

27 阅读4分钟

一、Vue的生命周期

Vue2的生命周期:

image.png 问题点

  1. 什么场景下会在mounted里调用接口,而不是created image.png
  2. 为什么不在beforeMount调用接口?
  • 答:与created没啥区别,那为什么不提前调用呢,减少白屏

Vue3的生命周期:

Vue 2Vue 3 (选项式)Vue 3 (组合式)说明
beforeCreate✅ beforeCreate❌ 无(直接用setup)实例初始化前
created✅ created❌ 无(直接用setup)实例创建后
beforeMount✅ beforeMountonBeforeMount挂载前
mounted✅ mountedonMounted挂载后
beforeUpdate✅ beforeUpdateonBeforeUpdate更新前
updated✅ updatedonUpdated更新后
beforeDestroy➡️ beforeUnmountonBeforeUnmount卸载前
destroyed➡️ unmountedonUnmounted卸载后
-✅ errorCapturedonErrorCaptured捕获错误
-✅ renderTrackedonRenderTracked调试:追踪依赖
-✅ renderTriggeredonRenderTriggered调试:触发重绘
activated✅ activatedonActivatedkeep-alive激活
deactivated✅ deactivatedonDeactivatedkeep-alive停用

问题点

  • setup替代了beforeCreate和created生命周期

  • onErrorCaptured用途和能捕获哪些错误:

    用途说明
    错误上报将错误发送到监控系统(如 Sentry)
    降级 UI显示错误提示,防止白屏
    错误隔离某个组件出错不影响其他部分
    日志记录记录用户操作路径,便于复现
    阻止错误传播控制错误是否继续向上传递

    ✅ 能捕获的错误类型

    错误类型能否捕获示例
    组件渲染错误✅ 能模板中调用不存在的方法
    生命周期钩子错误✅ 能mountedcreated 中的错误
    事件处理错误✅ 能@click 绑定的方法中抛错
    计算属性错误✅ 能计算属性中抛出异常
    watch 回调错误✅ 能监听的回调函数中抛错
    异步组件加载错误✅ 能动态 import() 失败
    setup 函数错误✅ 能组合式 API 中初始化错误

    ❌ 不能捕获的错误类型

    错误类型能否捕获原因
    异步任务中的错误❌ 不能setTimeoutPromise 内的错误(需自行 catch)
    自身组件的错误❌ 不能只能捕获后代组件,不能捕获自己
    事件总线错误❌ 不能非组件树的全局事件
    DOM 事件错误❌ 不能addEventListener 绑定的原生事件
    语法错误❌ 不能如 console.log(a.b)(a 未定义)在父组件

二、Vue的路由生命周期

分类钩子函数触发时机常用场景
全局守卫beforeEach路由跳转 权限验证、登录校验
beforeResolve所有组件内守卫解析完成后 确保数据加载完成
afterEach路由跳转 页面统计、标题修改
路由独享守卫beforeEnter进入该路由前 特定路由的权限控制
组件内守卫beforeRouteEnter进入组件前(不能访问this)进入前数据预加载
beforeRouteUpdate路由改变但组件复用时 动态路由参数变化时更新数据
beforeRouteLeave离开组件前 保存草稿、确认离开
// 全局前置守卫 - 登录验证
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

属性类型默认值说明
deepbooleanfalse是否深度监听(对象内部变化)
immediatebooleanfalse是否在创建时立即执行一次
flushstring'pre'回调执行时机:'pre'(组件更新前)、'post'(组件更新后)、'sync'(同步)
once (Vue 3.4+)booleanfalse是否只触发一次
// 基础监听
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什么场景下是函数

  • 组件内:组件会多次复用,防止数据污染,所以是函数(返回一个新的对象)
  • 根实例:不会复用

五、插槽

image.png

子组件:slotChild1.vue
<template>
  <div class="container">
    <slot></slot>
  </div>
</template>

父组件:
<div class="content">
    <span class="contentTitle">默认插槽</span>
    <slotChild1>
      <span>插槽内文本</span>
    </slotChild1>
</div>

image.png

子组件: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>

image.png

子组件: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)
  })
  方式2this.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不推荐)

image.png

image.png

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>

image.png

七、传值

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、refs/refs/ 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>

image.png

vue3差异化:

  1. 子组件接收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、等等

image.png

image.png

九、页面刷新后,出现白屏,如何优化

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
  }
})

image.png

4、添加全局错误处理

5、添加加载进度条

6、首屏加载优化

使用 CDN 加速

十、首页加载慢,如何优化

1、路由懒加载

2、第三方库按需引入+CDN外置

将固定依赖-vue/router/ui从打包中剔除,CDN加载

3、雪碧图/图片懒加载/图片压缩

4、优化白屏时间

5、代码优化

十一、vue有哪些常见指令

  1. v-bind(动态属性 :html)
  2. v-model
  3. v-if/v-else/v-else-if/v-show
  4. v-for
  5. v-on(简写@:@click="aa")
  6. v-html(渲染html字符串)
  7. v-text(等同于插值方式{{}})
  8. v-pre(跳过编译,显示原始内容)
  9. v-once(只渲染一次,后续数据变化不会更新)
  10. 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>

十三、attrsattrs、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>