Vue - 常见问题

578 阅读19分钟

1. Vue 生命周期详解 (Vue 2 vs Vue 3)

生命周期阶段Vue 2Vue 3 Composition API说明
创建前beforeCreate❌ 被 setup 替代组件实例初始化
创建完成created❌ 被 setup 替代组件数据方法初始化( data、methods、computed 等)
挂载前beforeMountonBeforeMount组件尚未挂载到 DOM , template 已解析
挂载后mountedonMounted组件挂载到 DOM ,可进行 DOM 操作、接口请求
更新前beforeUpdateonBeforeUpdate数据更新,DOM 未更新
更新后updatedonUpdated数据更新,DOM 更新
销毁前beforeDestroyonBeforeUnmount组件实例未销毁,做清理工作(如定时器、事件)
销毁后destroyedonUnmounted组件实例已销毁,所有绑定事件、子组件也被销毁
缓存激活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 响应式原理

对比点Vue2Vue3
核心 APIObject.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,视图不会更新
  1. 使用 Vue.setthis.$set

    Vue 会在内部将属性用 Object.defineProperty 添加到对象,并触发视图更新。

    this.$set(this.obj, 'b', 2)   // ✅ 响应式
    // 或者
    Vue.set(this.obj, 'b', 2)     // ✅ 响应式
    
  2. 使用 Object.assign{...} 替换整个数组

    Object.assign(this.obj, { b: 2 }) // ❌ 视图不更新,只是给现有对象加属性
    this.obj = Object.assign({}, this.obj, { b: 2 }) // ✅ 响应式,用新对象替换旧对象,视图就会更新
    this.obj = { ...this.obj, b: 2 } // ✅ 响应式,用新对象替换旧对象,视图就会更新
    
  3. 使用 this.$forceUpdate() 强制更新组件

    强制触发组件重新渲染,通常用于非响应式数据变更时,临时手动刷新视图。

    this.obj.b = 2 // ❌ 无响应
    this.$forceUpdate()    // ✅ 强制视图刷新
    
数组响应式更新
this.arr = [0]
this.arr[0] = 'new'    // ✅ 响应式
this.arr[1] = 'new'    // ❌ 不响应式
this.arr.length = 0          // ❌ 不响应式
  1. 使用 Vue.setthis.$set

    Vue.set(this.arr, 1, 'new')       // ✅ 响应式
    this.$set(this.arr, 1, 'new')     // ✅ 响应式
    
  2. 使用 [...] 替换整个数组

    this.arr = [...this.arr, 'new']  // ✅ 响应式(替换引用)
    
  3. 使用变异方法(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
    • 事件监听:监听 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. computedwatch 的区别

对比项computedwatch
主要用途计算属性,依赖其他数据监听属性变化,执行副作用逻辑
是否缓存✅ 有缓存,依赖不变不重新计算❌ 无缓存,每次变化都执行
返回值✅ 返回计算结果❌ 无返回值
支持异步操作❌ 不适合异步✅ 支持异步
是否能监听多个值❌ 一般用于一个表达式✅ 可以监听多个响应式属性(数组或函数形式)
适合使用场景用于响应式属性的计算用于执行异步操作、监听多个数据变化等

6. v-ifv-show 的区别

特性v-ifv-show
原理动态添加或移除 DOM 元素切换 DOM 元素的 display: none 样式
DOM 状态每次切换都会销毁和重建 DOMDOM 元素始终存在,只是显示隐藏
切换频率适合不频繁切换的场景适合频繁切换的场景

7. v-ifv-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-ifv-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:旧值(仅在 beforeUpdateupdated 中可用)。
    • arg:指令的参数(如 v-my-directive:arg)。
    • modifiers:修饰符对象(如 v-my-directive.modifier{ modifier: true })。
    • instance:使用指令的组件实例。
  • vnode:虚拟节点。
  • prevNode:上一个虚拟节点(仅在 beforeUpdateupdated 中可用)。

高级用法

  • 绑定多个值:通过对象传递多个参数

    <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 / injectprovide在祖先组件中提供数据,inject在后代组件中注入数据‌,适用于组件库开发、依赖注入等场景
attrs / listeners子组件中通过 $attrs 接收父组件未显式声明的属性,通过 $listeners 接收事件(Vue3 中整合到了 $attrs
插槽(slots)允许父组件向子组件插入模板内容,支持作用域插槽反向传递数据

Vue 的单向数据流

组件之间的数据流动,始终从 父组件 → 子组件 单向流动,子组件通过 props 接收数据(v-bind),不能直接修改,通过 $emit 通知父组件更新(v-on)。

好处:数据来源清晰,易于追踪;逻辑清晰,便于维护与调试;模块化,降低耦合;
特殊情况:传递数据的类型为引用类型,会存在副作用,即直接在子组件修改对象的属性,父组件会响应式更新。本质仍然是单向数据流,因为 props 的引用未变,仍然指向同一地址。


11. Vue3 中 setup 下的父子通信写法

方向方法子组件写法父组件写法
父 → 子PropsdefineProps:propName="value"
子 → 父EmitdefineEmits@event-name="handler"
子暴露给父ExposedefineExpose通过 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 等选项

  • 初始化 响应式数据(refreactive
  • 编写 业务逻辑函数(事件处理等)
  • 使用 生命周期函数(如 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

项目VuexPinia
支持版本Vue 2 / Vue 3Vue 3(支持 Vue 2 需插件)
API 风格Options API 风格Composition API 风格
核心概念state / getters / mutations / actions / modulesstate / getters / actions
状态修改方式必须通过 mutations 同步修改直接修改 state 或用 actions(更自然)
模块组织手动命名空间 + modules更自由,支持组合式模块化
代码体积大(~20KB)小(~1.5KB)

详细比较 Vuex Store 和 Pinia


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.routerouter 的区别

特性route(当前路由对象)router(路由实例)
作用提供当前路由的信息(路径、参数、查询等)用于编程式导航(跳转、前进、后退等)
常用属性path, params, query, hash, fullPathpush, replace, go, back, forward
示例route.params.idrouter.push('/user/123')

16. paramsquery 的区别

特性params(路径参数)query(查询参数)
使用场景动态路径参数查询参数(如分页、筛选)
表现形式/user/123/user?id=123
路由配置需在路由配置中使用 : 声明参数:path: '/user/:id'无需在路由路径中定义
刷新保留需在路由配置 props: true刷新后参数依然保留
获取方式(Vue2)this.$route.params.idthis.$route.query.id
获取方式(Vue3)useRoute().params.iduseRoute().query.id

适用场景

  • params
    • 用于标识资源的唯一性(如用户 ID /user/123)。
    • 需要提前在路由配置中定义参数(path: '/user/:id')。
  • query
    • 用于可选的过滤条件或分页(如 /products?category=books&page=2)。
    • 无需在路由配置中声明参数。

17. 路由守卫机制

Vue Router 的路由守卫(Navigation Guards) 用于在路由跳转过程中进行权限控制、数据预加载或取消导航等操作。

路由守卫类型

守卫类型执行时机使用场景方法
全局前置守卫路由跳转前触发(最早执行)全局登录验证beforeEach
路由独享守卫进入特定路由前触发路由权限控制beforeEnter
组件内守卫组件渲染前/更新时/离开前触发数据预加载数据保存提示beforeRouteEnterbeforeRouteLeave
全局解析守卫所有组件守卫和异步组件解析后触发确保异步数据加载完成beforeResolve
全局后置钩子导航完成后触发(无修改导航能力)页面访问统计、日志记录afterEach

执行顺序

全局 beforeEach → 路由独享 beforeEnter → 组件 beforeRouteEnter → 全局 beforeResolve → 全局 afterEach

关键参数说明

参数作用
to目标路由对象(包含 path, params, query, meta 等)
from正要离开的路由对象
next控制导航行为的函数:
- next():继续导航
- next(false):终止导航
- next('/path'):重定向

18. Hash 与 History 模式的区别

Vue Router 支持 HashHistory 两种模式。

对比项Hash 模式(默认模式)History 模式
描述URL 中 # 后拼接路由路径URL 中直接拼接路由路径
URL 形式/#/page/page
浏览器支持所有浏览器IE10+
是否美观否(含 #
SEO 友好性差,模拟路径爬虫难抓取好,真实路径 URL 结构清晰
是否需要服务端配置是(否则刷新 404)
原理hashchange 事件HTML5 History API

Hash 模式

  • 特点:

    • URL 形如:https://example.com/#/home
    • # 后的内容不会被浏览器发送给服务器,是模拟路径
    • 利用浏览器原生的 hashchange 事件监听地址变化
  • ✅ 优点:

    • 兼容性强,支持所有浏览器
    • 不需要服务端配合,直接部署即可访问
  • ❌ 缺点:

    • URL 带有 #,不美观
    • SEO 不友好,搜索引擎爬虫识别较差

History 模式

  • 特点:

    • URL 形如:https://example.com/home
    • 使用 HTML5 History API(如 pushState()popState()
  • ✅ 优点:

    • 路径更简洁美观
    • 真实路径,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 2Vue 3
    响应式原理Object.definePropertyProxy
    虚拟节点创建createElementh
    模板编译输出未静态提升静态 DOM 元素提升、静态属性对象预字符串化
    性能优化策略普通 Diffblock tree + patchFlag

    Vue3 的 h 函数:用于手动创建 VNode,替代模板语法:

    import { h } from 'vue';
    h('div', { class: 'box' }, 'Hello');
    

20. Vue 的 Diff 算法

  • Diff 算法:组件更新时,查找新旧虚拟 DOM 差异的方法。

  • 流程:

    • 生成新的虚拟 DOM 。
    • 比较新旧虚拟 DOM ,精确定位差异。
    • 将差异更新到真实 DOM 元素。
  • 利用 keytag 判断节点是否相同:

    • key :显式指定的唯一标识符。
    • tag :节点的标签名(如 divspan 或组件名)。
    • 相同:属性更新、事件更新、子节点对比。
    • 不同:销毁旧节点、创建新节点。
  • 采用深度优先 + 同层对比 + 双端比较的思路,提高效率。

  • 优点:

    • 复用已有 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 2Vue 3
使用方式Vue.nextTick / this.$nextTickimport { nextTick } from 'vue'
实现机制使用降级策略(多种异步方案兜底)直接使用 Promise.then() 实现
支持语法回调函数为主支持回调函数 + await 异步语法
Composition API不适用推荐配合 async setup() 使用

总结

$nextTick 是 Vue 提供的一种“等待 DOM 更新后再执行任务”的机制,本质上是利用微任务机制确保拿到的是最新、已 patch 的 DOM,避免操作旧的 DOM 状态。
页面真正发生视觉上的变化(Paint)还要等到下一帧由浏览器统一执行渲染任务,这是浏览器行为,不受 Vue 控制。


23. Vue 3 性能优化

优化点运行时收益
静态提升提出静态节点,只渲染一次
预字符串化静态属性对象(classstyle)预处理为字符串,减少拼接和合并
缓存事件处理函数稳定函数只创建一次,避免函数重建和 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)

  • 原理:在编译时,将模板中静态属性对象(如 classstyle),预处理为字符串,避免运行时拼接和合并。
  • 优点
    • 减少运行时计算开销
    • 渲染性能快
<template>
  <div class="static-class" style="color: red">Static</div>
</template>

静态属性对象classstyle将被编译为字符串,避免运行时合并类名或样式对象。

缓存事件处理函数(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 2Vue 3
响应式原理Object.definePropertyProxy
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 APIComposition 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 工作原理

  1. 基于 ES Module(import / export,静态分析导入导出关系。
  2. 常用于 webpack、Vite 等构建工具,扫描哪些模块的导出部分没有被使用。
  3. 移除未使用的导出部分,不打包到最终输出文件中。
// 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(代码分割)将代码拆分多个模块,按需加载提升首屏加载速度