Vue.js 面试题整理

本文档系统整理了 Vue.js 常见面试题,涵盖 Vue2 和 Vue3 的核心知识点,包括基础概念、组件通信、生命周期、响应式原理、Composition API 等内容。每个问题都标注了难度等级(⭐),并提供详细的答案解析和代码示例。

📑 目录


一、Vue 基础概念

1.1 什么是前端构建工具

难度:⭐⭐

问题描述

什么是前端构建工具?比如 Vue2 的 webpack,Vue3 的 Vite。

答案解析

核心概念: 前端构建工具是帮助开发者将浏览器不认识的代码(TypeScript、Vue、Less 等)自动转换为浏览器可执行代码的工具。

背景说明:

  • 浏览器只认识 HTML、CSS、JavaScript
  • 现代项目使用 TypeScript、Vue/React、Less/Sass、ES6+ 等技术
  • 需要各种编译工具:tsc、vue-compiler、less-loader、babel、uglifyjs 等

构建工具的核心功能:

  1. 模块化开发支持

    • 支持从 node_modules 引入代码
    • 支持多种模块化规范(CommonJS、ES Module 等)
  2. 代码兼容性处理

    • TypeScript → JavaScript
    • Vue/React → JavaScript
    • Less/Sass → CSS
    • ES6+ → ES5(通过 Babel)
  3. 性能优化

    • 代码压缩(uglifyjs)
    • 代码分割(Code Splitting)
    • Tree Shaking(移除未使用代码)
  4. 开发体验优化

    • 热更新(Hot Module Replacement)
    • 开发服务器(解决跨域问题)
    • 自动监听文件变化并重新编译

工作流程:

源代码变化 → 构建工具自动检测 → 调用对应编译工具 → 生成浏览器可执行代码 → 自动刷新浏览器

常见构建工具:

  • Webpack:功能强大,配置复杂,适合大型项目
  • Vite:基于 ES Module,开发时按需编译,启动速度快
  • Rollup:适合库的打包
  • Parcel:零配置,开箱即用

核心要点:

构建工具让开发者专注于业务逻辑,无需关心代码如何在浏览器运行,只需要写最舒服的代码即可。


1.2 MVVM 和 MVC 的原理及区别

难度:⭐⭐⭐

问题描述

简述 MVVM 和 MVC 的原理以及它们的区别。

答案解析
MVVM(Model-View-ViewModel)

核心特点: 视图模型双向绑定

组成部分:

  • Model(模型):数据层,负责业务逻辑和数据
  • View(视图):UI 层,负责页面展示
  • ViewModel(视图模型):连接 Model 和 View,实现数据双向绑定

工作原理:

View ←→ ViewModel ←→ Model
     双向绑定      数据处理

优点:

  1. 低耦合:View 可以独立于 Model 变化和修改
  2. 可重用性:一个 ViewModel 可以绑定到不同的 View
  3. 独立开发:前端专注 View,后端专注 Model
  4. 可测试性:ViewModel 易于单元测试

Vue 中的 MVVM:

<template>
  <!-- View 视图层 -->
  <div>{{ message }}</div>
  <input v-model="message" />
</template>

<script>
export default {
  data() {
    // Model 数据层
    return {
      message: 'Hello Vue'
    }
  }
  // Vue 实例充当 ViewModel
}
</script>
MVC(Model-View-Controller)

核心特点: 单向通信,通过 Controller 协调

组成部分:

  • Model(模型):数据和业务逻辑(如数据库操作)
  • View(视图):页面展示(HTML)
  • Controller(控制器):处理用户交互,连接 Model 和 View

工作原理:

View → Controller → Model
  ↑                   ↓
  └───────────────────┘
      单向通信

优点:

  1. 低耦合
  2. 重用性高
  3. 生命周期成本低
  4. 可维护性高
  5. 有利于软件工程化管理
MVVM 与 MVC 的区别
对比项MVCMVVM
通信方式单向通信(View → Controller → Model)双向绑定(View ↔ ViewModel ↔ Model)
核心角色Controller 处理业务逻辑ViewModel 实现数据绑定
DOM 操作需要手动操作 DOM自动同步,无需手动操作
数据流必须通过 Controller 传递数据自动同步
适用场景传统 Web 应用现代前端框架(Vue、React)

核心要点:

MVVM 通过数据驱动视图,解决了 MVC 中大量 DOM 操作导致的性能问题,提升了开发效率和用户体验。


1.3 Vue 常见指令

难度:⭐⭐⭐

问题描述

列举 Vue 中常用的指令及其作用。

答案解析
数据绑定指令

1. v-model

  • 作用:实现表单元素的双向数据绑定
  • 适用:input、textarea、select 等表单元素
<template>
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello'
    }
  }
}
</script>

2. v-bind(简写 :

  • 作用:动态绑定属性
  • 类型:字符串、对象、数组
<template>
  <!-- 绑定属性 -->
  <img :src="imageUrl" />

  <!-- 绑定 class -->
  <div :class="{ active: isActive }"></div>
  <div :class="[classA, classB]"></div>

  <!-- 绑定 style -->
  <div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
</template>
事件绑定指令

3. v-on(简写 @

  • 作用:绑定事件监听器
<template>
  <button @click="handleClick">点击</button>
  <input @input="handleInput" />
  <form @submit.prevent="handleSubmit">提交</form>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('clicked')
    },
    handleInput(e) {
      console.log(e.target.value)
    },
    handleSubmit() {
      // .prevent 阻止默认行为
    }
  }
}
</script>
条件渲染指令

4. v-if / v-else-if / v-else

  • 作用:条件渲染,控制元素是否渲染到 DOM
  • 特点:条件为 false 时,元素不会渲染
<template>
  <div v-if="type === 'A'">A</div>
  <div v-else-if="type === 'B'">B</div>
  <div v-else>C</div>
</template>

5. v-show

  • 作用:控制元素显示/隐藏
  • 特点:通过 CSS display 属性控制,元素始终渲染
<template>
  <div v-show="isVisible">显示内容</div>
</template>

v-if vs v-show:

  • v-if:切换开销高,适合条件不频繁变化
  • v-show:初始渲染开销高,适合频繁切换
列表渲染指令

6. v-for

  • 作用:循环渲染列表
  • 注意:必须添加唯一 key 属性
<template>
  <!-- 遍历数组 -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }} - {{ item.name }}
  </li>

  <!-- 遍历对象 -->
  <div v-for="(value, key, index) in object" :key="key">
    {{ key }}: {{ value }}
  </div>
</template>
其他常用指令

7. v-text

  • 作用:更新元素的文本内容(等同于 {{ }}
<span v-text="message"></span>
<!-- 等同于 -->
<span>{{ message }}</span>

8. v-html

  • 作用:渲染 HTML 内容
  • 警告:谨防 XSS 攻击
<div v-html="htmlContent"></div>

9. v-once

  • 作用:只渲染一次,后续不再更新
<span v-once>{{ message }}</span>

10. v-cloak

  • 作用:防止页面闪烁(配合 CSS 使用)
<style>
[v-cloak] {
  display: none;
}
</style>

<div v-cloak>{{ message }}</div>

11. v-pre

  • 作用:跳过编译,原样输出
<span v-pre>{{ 这里不会被编译 }}</span>

核心要点:

Vue 指令是带有 v- 前缀的特殊属性,用于在模板中声明式地绑定数据、事件和 DOM 操作。


1.4 data 为什么必须是函数

难度:⭐⭐⭐

问题描述

在 Vue 组件中,data 选项为什么必须是一个函数而不能是对象?

答案解析

核心原因: 保证每个组件实例拥有独立的数据对象,避免数据共享污染。

问题场景

如果 data 是对象(错误示例):

// ❌ 错误:所有实例共享同一个 data 对象
export default {
  data: {
    count: 0
  }
}

问题: 当创建多个组件实例时,它们会共享同一个 data 对象,导致:

  • 修改一个实例的数据,其他实例也会受影响
  • 数据互相污染,无法隔离
正确做法

data 必须是函数:

// ✅ 正确:每个实例都有独立的 data 对象
export default {
  data() {
    return {
      count: 0
    }
  }
}

原理: 每次创建组件实例时,都会调用 data 函数,返回一个新的数据对象,确保数据隔离。

完整示例
<!-- Counter.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="increment">{{ count }}</button>
  </div>
</template>

<script>
export default {
  data() {
    // 每个实例调用时都会返回新对象
    return {
      message: 'Hello, Vue!',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>
<!-- App.vue -->
<template>
  <div>
    <!-- 三个独立的 Counter 实例,数据互不影响 -->
    <Counter />
    <Counter />
    <Counter />
  </div>
</template>

<script>
import Counter from './Counter.vue'

export default {
  components: { Counter }
}
</script>
特殊情况

根实例可以使用对象:

// 根实例只会创建一次,可以使用对象
new Vue({
  data: {
    count: 0
  }
})
函数参数的高级用法

data 函数可以接收组件实例作为参数:

export default {
  props: ['initialCount'],
  data(vm) {
    // vm 是组件实例
    return {
      count: vm.initialCount || 0
    }
  }
}

核心要点:

data 必须是函数,确保每个组件实例都有独立的数据副本,这是 Vue 组件化设计的基础。


1.5 ref 的作用

难度:⭐⭐⭐

问题描述

Vue 中 ref 的作用是什么?如何使用?

答案解析

核心作用: ref 用于获取 DOM 元素或子组件实例的引用。

使用场景

1. 获取 DOM 元素引用

<template>
  <div>
    <div ref="testDom">DOM 元素</div>
    <button @click="getElement">获取元素</button>
  </div>
</template>

<script>
export default {
  methods: {
    getElement() {
      // 通过 this.$refs 访问
      console.log(this.$refs.testDom)
      // 可以直接操作 DOM
      this.$refs.testDom.style.color = 'red'
    }
  }
}
</script>

2. 获取子组件实例

<!-- 父组件 -->
<template>
  <div>
    <Child ref="childRef" />
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  components: { Child },
  mounted() {
    // 访问子组件的数据
    console.log(this.$refs.childRef.message)
    // 调用子组件的方法
    this.$refs.childRef.sayHello()
  },
  methods: {
    callChildMethod() {
      this.$refs.childRef.sayHello()
    }
  }
}
</script>
<!-- 子组件 Child.vue -->
<template>
  <div>子组件</div>
</template>

<script>
export default {
  data() {
    return {
      message: '来自子组件的数据'
    }
  },
  methods: {
    sayHello() {
      console.log('Hello from child!')
    }
  }
}
</script>

3. 在 v-for 中使用 ref

<template>
  <div>
    <div
      v-for="(item, index) in items"
      :key="item.id"
      :ref="'item' + index"
    >
      {{ item.name }}
    </div>
    <button @click="getItems">获取所有元素</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    }
  },
  methods: {
    getItems() {
      // ref 在 v-for 中会是一个数组
      this.items.forEach((item, index) => {
        console.log(this.$refs['item' + index])
      })
    }
  }
}
</script>
Vue3 中的 ref

在 Vue3 Composition API 中,ref 的使用方式有所不同:

<template>
  <div>
    <div ref="elementRef">DOM 元素</div>
    <Child ref="childRef" />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

// 声明 ref(初始值为 null)
const elementRef = ref(null)
const childRef = ref(null)

onMounted(() => {
  // 组件挂载后才能访问
  console.log(elementRef.value) // DOM 元素
  console.log(childRef.value)   // 子组件实例
})
</script>
注意事项

1. 访问时机

  • ref 需要在 DOM 渲染完成后才能访问
  • mounted 生命周期或 onMounted 中使用
  • 或在 this.$nextTick() 中使用
export default {
  mounted() {
    // ✅ 正确:DOM 已渲染
    console.log(this.$refs.myElement)
  },
  created() {
    // ❌ 错误:DOM 未渲染
    console.log(this.$refs.myElement) // undefined
  }
}

2. v-for 中的 ref

  • 如果 ref 用在 v-for 中,$refs 会是一个数组
  • 需要遍历访问

3. 响应式注意

  • ref 引用本身不是响应式的
  • 不要在模板中依赖 ref 的值

核心要点:

ref 提供了直接访问 DOM 和组件实例的能力,但应谨慎使用,优先使用数据驱动的方式。


二、组件通信

2.1 Vue2 组件通信方式

难度:⭐⭐⭐⭐⭐

问题描述

Vue2 中组件之间有哪些通信方式?

答案解析
1. 父传子:Props

父组件:

<!-- App.vue -->
<template>
  <div id="app">
    <Child :users="users" :count="count" />
  </div>
</template>

<script>
import Child from './components/Child.vue'

export default {
  name: 'App',
  data() {
    return {
      users: ['Alice', 'Bob', 'Charlie'],
      count: 100
    }
  },
  components: {
    Child
  }
}
</script>

子组件:

<!-- Child.vue -->
<template>
  <div class="child">
    <ul>
      <li v-for="user in users" :key="user">{{ user }}</li>
    </ul>
    <p>Count: {{ count }}</p>
  </div>
</template>

<script>
export default {
  name: 'Child',
  props: {
    users: {
      type: Array,
      required: true
    },
    count: {
      type: Number,
      default: 0
    }
  }
}
</script>
2. 子传父:$emit

子组件:

<!-- Header.vue -->
<template>
  <header>
    <h1 @click="changeTitle">{{ title }}</h1>
  </header>
</template>

<script>
export default {
  name: 'Header',
  data() {
    return {
      title: 'Vue.js Demo'
    }
  },
  methods: {
    changeTitle() {
      // 触发自定义事件,传递数据
      this.$emit('titleChanged', '子组件传递的新标题')
    }
  }
}
</script>

父组件:

<!-- App.vue -->
<template>
  <div id="app">
    <!-- 监听子组件的自定义事件 -->
    <Header @titleChanged="updateTitle" />
    <h2>{{ title }}</h2>
  </div>
</template>

<script>
import Header from './components/Header.vue'

export default {
  name: 'App',
  data() {
    return {
      title: '初始标题'
    }
  },
  methods: {
    updateTitle(newTitle) {
      // 接收子组件传递的数据
      this.title = newTitle
    }
  },
  components: {
    Header
  }
}
</script>
3. 兄弟组件通信:事件总线(EventBus)
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
<!-- ComponentA.vue -->
<template>
  <button @click="sendMessage">发送消息</button>
</template>

<script>
import { EventBus } from './eventBus.js'

export default {
  methods: {
    sendMessage() {
      EventBus.$emit('message', 'Hello from A')
    }
  }
}
</script>
<!-- ComponentB.vue -->
<template>
  <div>{{ message }}</div>
</template>

<script>
import { EventBus } from './eventBus.js'

export default {
  data() {
    return {
      message: ''
    }
  },
  mounted() {
    EventBus.$on('message', (msg) => {
      this.message = msg
    })
  },
  beforeDestroy() {
    // 组件销毁前移除监听
    EventBus.$off('message')
  }
}
</script>
4. 跨层级通信:provide / inject
<!-- 祖先组件 -->
<script>
export default {
  provide() {
    return {
      theme: 'dark',
      user: this.currentUser
    }
  },
  data() {
    return {
      currentUser: { name: '张三' }
    }
  }
}
</script>
<!-- 后代组件(任意层级) -->
<script>
export default {
  inject: ['theme', 'user'],
  mounted() {
    console.log(this.theme) // 'dark'
    console.log(this.user)  // { name: '张三' }
  }
}
</script>
5. 通过 $parent / $children 访问
// 子组件访问父组件
this.$parent.parentMethod()
this.$parent.parentData

// 父组件访问子组件
this.$children[0].childMethod()
this.$children[0].childData

注意: 不推荐使用,耦合度高,难以维护。

核心要点:

Vue2 提供了多种组件通信方式,应根据组件关系选择合适的方法:父子用 props/$emit,跨层级用 provide/inject,全局状态用 Vuex。


2.2 Vuex 的理解及使用场景

难度:⭐⭐⭐

问题描述

什么是 Vuex?它的核心概念有哪些?适用于什么场景?

答案解析

核心概念: Vuex 是 Vue 应用程序的状态管理模式,采用集中式存储管理应用的所有组件状态。

Vuex 的核心组成

1. State(状态)

  • 定义应用的状态数据
  • 单一状态树,唯一数据源
const store = new Vuex.Store({
  state: {
    count: 0,
    user: {
      name: '张三',
      age: 20
    }
  }
})

// 组件中访问
this.$store.state.count

2. Getter(计算属性)

  • 从 state 派生出的状态
  • 类似于组件的计算属性
  • 结果会被缓存
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '学习 Vue', done: true },
      { id: 2, text: '学习 Vuex', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    }
  }
})

// 组件中访问
this.$store.getters.doneTodos

3. Mutation(同步修改状态)

  • 唯一修改 state 的方法
  • 必须是同步函数
  • 通过 commit 触发
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    incrementBy(state, payload) {
      state.count += payload.amount
    }
  }
})

// 组件中调用
this.$store.commit('increment')
this.$store.commit('incrementBy', { amount: 10 })

4. Action(异步操作)

  • 提交 mutation,而不是直接变更状态
  • 可以包含任意异步操作
  • 通过 dispatch 触发
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    },
    // 异步请求示例
    async fetchUser({ commit }, userId) {
      const response = await fetch(`/api/user/${userId}`)
      const user = await response.json()
      commit('setUser', user)
    }
  }
})

// 组件中调用
this.$store.dispatch('incrementAsync')

5. Module(模块化)

  • 将 store 分割成模块
  • 每个模块拥有自己的 state、mutation、action、getter
const moduleA = {
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {},
  getters: {}
}

const moduleB = {
  state: () => ({ count: 0 }),
  mutations: {},
  actions: {}
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

// 访问模块状态
store.state.a.count
store.state.b.count
完整示例
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    todos: []
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  },
  mutations: {
    increment(state) {
      state.count++
    },
    addTodo(state, todo) {
      state.todos.push(todo)
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  modules: {}
})
<!-- 组件中使用 -->
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Done Todos: {{ doneTodos.length }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementAsync">异步+1</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 映射 state
    ...mapState(['count', 'todos']),
    // 映射 getters
    ...mapGetters(['doneTodos'])
  },
  methods: {
    // 映射 mutations
    ...mapMutations(['increment']),
    // 映射 actions
    ...mapActions(['incrementAsync'])
  }
}
</script>
使用场景

适合使用 Vuex:

  1. 多个组件共享状态
  2. 多个组件需要修改同一状态
  3. 需要跨页面保持状态
  4. 复杂的状态管理逻辑
  5. 中大型单页应用

不适合使用 Vuex:

  1. 简单的父子组件通信(用 props/emit)
  2. 小型应用(状态管理简单)
  3. 临时性的状态(用组件内部 data)

核心要点:

Vuex 通过集中式状态管理,解决了多组件共享状态的问题。核心原则是:State 只能通过 Mutation 修改,异步操作放在 Action 中。


2.3 Vue3 组件通信方式

难度:⭐⭐⭐⭐⭐

问题描述

Vue3 中组件通信方式有哪些?与 Vue2 有什么区别?

答案解析

Vue3 在保留 Vue2 通信方式的基础上,新增了 Composition API 相关的方法。

1. 父传子:Props(defineProps)
<!-- 父组件 -->
<template>
  <ChildComponent :message="parentMsg" :user="userInfo" />
</template>

<script setup>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentMsg = ref('来自父组件的消息')
const userInfo = reactive({ name: '张三', age: 20 })
</script>
<!-- 子组件 -->
<template>
  <div>
    <p>{{ message }}</p>
    <p>{{ user.name }} - {{ user.age }}</p>
  </div>
</template>

<script setup>
import { defineProps } from 'vue'

// 方式1:简单声明
const props = defineProps(['message', 'user'])

// 方式2:带类型校验(推荐)
const props = defineProps({
  message: {
    type: String,
    required: true
  },
  user: {
    type: Object,
    default: () => ({})
  }
})

// 使用 props(无需 .value)
console.log(props.message)
</script>
2. 子传父:Emits(defineEmits)
<!-- 子组件 -->
<template>
  <button @click="handleClick">发送数据</button>
</template>

<script setup>
import { defineEmits } from 'vue'

// 定义可触发的事件
const emit = defineEmits(['sendData', 'update'])

// 或带类型校验
const emit = defineEmits({
  sendData: (data) => typeof data === 'string',
  update: (num) => num > 0
})

const handleClick = () => {
  emit('sendData', '来自子组件的消息')
  emit('update', 100)
}
</script>
<!-- 父组件 -->
<template>
  <ChildComponent
    @sendData="onReceive"
    @update="onUpdate"
  />
</template>

<script setup>
const onReceive = (data) => {
  console.log('收到子组件数据:', data)
}

const onUpdate = (num) => {
  console.log('收到更新值:', num)
}
</script>
3. 父子双向绑定:v-model

单值绑定:

<!-- 父组件 -->
<template>
  <ChildComponent v-model="count" />
  <!-- 等价于 <ChildComponent :modelValue="count" @update:modelValue="count = $event" /> -->
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- 子组件 -->
<template>
  <button @click="handleAdd">count: {{ modelValue }}</button>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const handleAdd = () => {
  emit('update:modelValue', props.modelValue + 1)
}
</script>

多值绑定:

<!-- 父组件 -->
<template>
  <ChildComponent
    v-model:name="userName"
    v-model:age="userAge"
  />
</template>

<script setup>
import { ref } from 'vue'
const userName = ref('张三')
const userAge = ref(20)
</script>
<!-- 子组件 -->
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps(['name', 'age'])
const emit = defineEmits(['update:name', 'update:age'])

const updateName = (newName) => {
  emit('update:name', newName)
}

const updateAge = (newAge) => {
  emit('update:age', newAge)
}
</script>
4. 跨层级通信:Provide / Inject
<!-- 顶层组件(提供数据) -->
<script setup>
import { provide, ref } from 'vue'

// 提供普通值
provide('theme', 'dark')

// 提供响应式数据(子组件会同步更新)
const user = ref({ name: '张三' })
provide('currentUser', user)

// 提供修改方法
const updateUser = (newName) => {
  user.value.name = newName
}
provide('updateUser', updateUser)
</script>
<!-- 深层子组件(接收数据) -->
<template>
  <div>
    <p>Theme: {{ theme }}</p>
    <p>User: {{ currentUser.name }}</p>
    <button @click="updateUser('李四')">更新用户</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 接收数据(第二个参数为默认值)
const theme = inject('theme', 'light')
const currentUser = inject('currentUser')
const updateUser = inject('updateUser')
</script>
5. 全局状态管理:Pinia

Vue3 推荐使用 Pinia 替代 Vuex。

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: { name: '张三' }
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchUser() {
      const response = await fetch('/api/user')
      this.user = await response.json()
    }
  }
})
<!-- 任意组件使用 -->
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">+1</button>
  </div>
</template>

<script setup>
import { useCounterStore } from './stores/counter'

const counter = useCounterStore()

// 访问状态
console.log(counter.count)

// 修改状态(直接修改)
counter.count++

// 或调用 action
counter.increment()
</script>
6. 通过 ref 访问子组件
<!-- 父组件 -->
<template>
  <Child ref="childRef" />
  <button @click="callChild">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

const callChild = () => {
  // 访问子组件暴露的方法和数据
  childRef.value.childMethod()
  console.log(childRef.value.childData)
}
</script>
<!-- 子组件 -->
<script setup>
import { ref, defineExpose } from 'vue'

const childData = ref('子组件数据')

const childMethod = () => {
  console.log('子组件方法被调用')
}

// 必须显式暴露给父组件
defineExpose({
  childData,
  childMethod
})
</script>
Vue2 vs Vue3 通信方式对比
通信方式Vue2Vue3
Propsprops 选项defineProps()
Emits$emit()defineEmits()
v-model单值绑定支持多值绑定
Provide/Injectprovide/inject 选项provide()/inject()
状态管理VuexPinia(推荐)
访问子组件$refsref + defineExpose
事件总线EventBusmitt 库

核心要点:

Vue3 通过 Composition API 提供了更灵活的组件通信方式,推荐使用 defineProps、defineEmits、provide/inject 和 Pinia 进行状态管理。


三、生命周期

3.1 Vue2 生命周期八个阶段

难度:⭐⭐⭐⭐

问题描述

Vue2 的生命周期有哪八个阶段?每个阶段的特点是什么?

答案解析

Vue2 组件从创建到销毁经历 8 个生命周期钩子函数。

创建阶段

1. beforeCreate(创建前)

  • 时机:实例初始化之后,数据观测和事件配置之前
  • 特点
    • data、methods、computed、watch 都不可用
    • 只有生命周期函数和默认事件
  • 用途:很少使用,一般用于插件开发
export default {
  beforeCreate() {
    console.log('beforeCreate')
    console.log(this.message) // undefined
    console.log(this.getData) // undefined
  }
}

2. created(创建后)

  • 时机:实例创建完成,数据观测、属性和方法的运算已完成
  • 特点:
    • data、methods、computed、watch 已初始化
    • DOM 还未生成,$el 不可用
  • 用途
    • 调用 methods 中的方法
    • 操作 data 中的数据
    • 发起异步请求(推荐)
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  created() {
    console.log('created')
    console.log(this.message) // 'Hello'
    // 适合发起 API 请求
    this.fetchData()
  },
  methods: {
    fetchData() {
      // 发起请求
    }
  }
}
挂载阶段

3. beforeMount(挂载前)

  • 时机:模板编译完成,虚拟 DOM 已创建,但未挂载到页面
  • 特点
    • 模板已在内存中编译
    • 页面还未渲染
    • $el 已存在但是旧的
  • 用途:很少使用
export default {
  beforeMount() {
    console.log('beforeMount')
    console.log(this.$el) // 存在但未更新
  }
}

4. mounted(挂载后)

  • 时机:实例挂载到 DOM,页面渲染完成
  • 特点
    • DOM 已渲染完成
    • 可以访问 DOM 元素
    • 可以操作 $refs
  • 用途
    • 操作 DOM
    • 使用第三方 DOM 库
    • 发起异步请求(也可以)
export default {
  mounted() {
    console.log('mounted')
    // 可以访问 DOM
    console.log(this.$refs.myElement)
    // 可以操作 DOM
    this.$refs.myElement.style.color = 'red'
  }
}
更新阶段

5. beforeUpdate(更新前)

  • 时机:数据更新时调用,发生在虚拟 DOM 重新渲染之前
  • 特点
    • data 中的数据是最新的
    • 页面显示的数据是旧的
    • 页面和数据尚未同步
  • 用途
    • 在更新前访问现有的 DOM
    • 移除手动添加的事件监听器
export default {
  data() {
    return {
      count: 0
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate')
    console.log('data:', this.count) // 最新值
    console.log('DOM:', this.$refs.count.textContent) // 旧值
  }
}

6. updated(更新后)

  • 时机:数据更改导致的虚拟 DOM 重新渲染和打补丁之后
  • 特点
    • data 和页面都是最新的
    • DOM 已更新完成
  • 用途
    • 执行依赖于 DOM 的操作
  • 注意:避免在此更改状态,可能导致无限循环
export default {
  updated() {
    console.log('updated')
    console.log('data:', this.count) // 最新值
    console.log('DOM:', this.$refs.count.textContent) // 最新值

    // ❌ 避免在这里修改数据
    // this.count++ // 会导致无限循环
  }
}
销毁阶段

7. beforeDestroy(销毁前)

  • 时机:实例销毁之前调用
  • 特点
    • 实例仍然完全可用
    • data、methods 等都可用
  • 用途
    • 清除定时器
    • 解绑自定义事件
    • 取消网络请求
    • 清理手动创建的 DOM
export default {
  data() {
    return {
      timer: null
    }
  },
  mounted() {
    this.timer = setInterval(() => {
      console.log('tick')
    }, 1000)
  },
  beforeDestroy() {
    console.log('beforeDestroy')
    // 清除定时器
    if (this.timer) {
      clearInterval(this.timer)
    }
    // 解绑事件
    window.removeEventListener('resize', this.handleResize)
  }
}

8. destroyed(销毁后)

  • 时机:实例销毁后调用
  • 特点
    • 所有指令解绑
    • 所有事件监听器移除
    • 所有子实例销毁
    • data、methods 等不可用
  • 用途:很少使用
export default {
  destroyed() {
    console.log('destroyed')
    // 组件已完全销毁
  }
}
生命周期流程图
创建阶段:
  new Vue()
    ↓
  beforeCreate  → 初始化事件和生命周期
    ↓
  created       → 初始化 data、methods、computed、watch
    ↓
挂载阶段:
  beforeMount   → 编译模板,生成虚拟 DOM
    ↓
  mounted       → 挂载到真实 DOM
    ↓
更新阶段(数据变化时):
  beforeUpdate  → 数据更新,虚拟 DOM 重新渲染前
    ↓
  updated       → 虚拟 DOM 重新渲染并打补丁后
    ↓
销毁阶段:
  beforeDestroy → 实例销毁前
    ↓
  destroyed     → 实例销毁后

核心要点:

Vue2 生命周期分为创建、挂载、更新、销毁四个阶段。created 适合数据初始化和 API 请求,mounted 适合 DOM 操作,beforeDestroy 适合清理工作。


3.2 生命周期的使用场景

难度:⭐⭐⭐

问题描述

Vue 生命周期各个阶段分别适合做什么?

答案解析
beforeCreate
  • 适用场景:很少使用
  • 可以做
    • 加载 loading 事件
    • 插件开发时的初始化
  • 不能做
    • 访问 data、methods(还未初始化)
created
  • 适用场景:最常用的初始化阶段
  • 可以做
    • ✅ 发起 API 请求(推荐)
    • ✅ 操作 data 中的数据
    • ✅ 调用 methods 中的方法
    • ✅ 初始化非响应式变量
  • 不能做
    • ❌ 操作 DOM(还未渲染)
    • ❌ 访问 $refs(还未挂载)
export default {
  data() {
    return {
      userList: []
    }
  },
  created() {
    // ✅ 推荐:发起 API 请求
    this.fetchUserList()
  },
  methods: {
    async fetchUserList() {
      const res = await fetch('/api/users')
      this.userList = await res.json()
    }
  }
}
beforeMount
  • 适用场景:很少使用
  • 可以做
    • 在渲染前最后一次修改数据
  • 不能做
    • 操作 DOM(还未挂载)
mounted
  • 适用场景:需要操作 DOM 时
  • 可以做
    • ✅ 操作 DOM 元素
    • ✅ 访问 $refs
    • ✅ 使用第三方 DOM 库(如 ECharts、Swiper)
    • ✅ 发起 API 请求(也可以,但推荐在 created)
    • ✅ 添加事件监听器
export default {
  mounted() {
    // ✅ 操作 DOM
    this.$refs.input.focus()

    // ✅ 使用第三方库
    this.chart = echarts.init(this.$refs.chartContainer)

    // ✅ 添加事件监听
    window.addEventListener('resize', this.handleResize)
  }
}
beforeUpdate
  • 适用场景:很少使用
  • 可以做
    • 在更新前访问现有的 DOM
    • 移除手动添加的事件监听器
  • 注意
    • 不要在这里修改数据(会再次触发更新)
updated
  • 适用场景:需要在 DOM 更新后执行操作
  • 可以做
    • ✅ 执行依赖于 DOM 的操作
    • ✅ 访问更新后的 DOM
  • 不能做
    • ❌ 修改数据(会导致无限循环)
export default {
  data() {
    return {
      scrollToBottom: false
    }
  },
  updated() {
    // ✅ DOM 更新后滚动到底部
    if (this.scrollToBottom) {
      this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight
      this.scrollToBottom = false
    }
  }
}
beforeDestroy
  • 适用场景:组件销毁前的清理工作(最重要)
  • 可以做
    • ✅ 清除定时器
    • ✅ 解绑自定义事件
    • ✅ 取消网络请求
    • ✅ 移除事件监听器
    • ✅ 清理手动创建的 DOM
export default {
  data() {
    return {
      timer: null,
      eventBus: null
    }
  },
  mounted() {
    // 创建定时器
    this.timer = setInterval(() => {
      this.updateTime()
    }, 1000)

    // 添加事件监听
    window.addEventListener('resize', this.handleResize)

    // 监听事件总线
    this.eventBus = EventBus.$on('update', this.handleUpdate)
  },
  beforeDestroy() {
    // ✅ 清除定时器
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }

    // ✅ 移除事件监听
    window.removeEventListener('resize', this.handleResize)

    // ✅ 解绑事件总线
    EventBus.$off('update', this.handleUpdate)
  }
}
destroyed
  • 适用场景:很少使用
  • 可以做
    • 确认清理完成
    • 发送销毁统计
常见使用场景总结
场景推荐生命周期原因
发起 API 请求created越早获取数据越好,减少白屏时间
操作 DOMmountedDOM 已渲染完成
使用第三方 DOM 库mounted需要真实 DOM 元素
清除定时器beforeDestroy防止内存泄漏
移除事件监听beforeDestroy防止内存泄漏
初始化数据createddata 已初始化
访问 $refsmounted组件已挂载

核心要点:

created 适合数据初始化和 API 请求,mounted 适合 DOM 操作,beforeDestroy 必须做清理工作防止内存泄漏。


3.3 Vue2 和 Vue3 生命周期对比

难度:⭐⭐⭐⭐⭐

问题描述

Vue2 和 Vue3 的生命周期有什么区别?

答案解析
Options API 对比

Vue3 保留了 Vue2 的生命周期钩子,但有两个重命名:

Vue2Vue3说明
beforeCreatebeforeCreate创建前
createdcreated创建后
beforeMountbeforeMount挂载前
mountedmounted挂载后
beforeUpdatebeforeUpdate更新前
updatedupdated更新后
beforeDestroybeforeUnmount⚠️ 重命名:卸载前
destroyedunmounted⚠️ 重命名:卸载后
<!-- Vue3 Options API -->
<script>
export default {
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  },
  beforeUnmount() {  // ⚠️ Vue2 中是 beforeDestroy
    console.log('beforeUnmount')
  },
  unmounted() {      // ⚠️ Vue2 中是 destroyed
    console.log('unmounted')
  }
}
</script>
Composition API 生命周期

Vue3 新增了 Composition API 形式的生命周期钩子:

Options APIComposition API说明
beforeCreate❌ 不需要使用 setup() 替代
created❌ 不需要使用 setup() 替代
beforeMountonBeforeMount挂载前
mountedonMounted挂载后
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeUnmountonBeforeUnmount卸载前
unmountedonUnmounted卸载后

特殊说明:

  • setup()beforeCreatecreated 之间执行
  • setup() 中可以直接初始化数据,无需 beforeCreatecreated
<!-- Vue3 Composition API -->
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

// setup() 阶段(相当于 beforeCreate + created)
const count = ref(0)
console.log('setup 阶段')

onBeforeMount(() => {
  console.log('onBeforeMount')
})

onMounted(() => {
  console.log('onMounted')
  // 操作 DOM
})

onBeforeUpdate(() => {
  console.log('onBeforeUpdate')
})

onUpdated(() => {
  console.log('onUpdated')
})

onBeforeUnmount(() => {
  console.log('onBeforeUnmount')
  // 清理工作
})

onUnmounted(() => {
  console.log('onUnmounted')
})
</script>
完整对比示例

Vue2 写法:

<script>
export default {
  data() {
    return {
      count: 0,
      timer: null
    }
  },
  created() {
    console.log('created')
    this.fetchData()
  },
  mounted() {
    console.log('mounted')
    this.timer = setInterval(() => {
      this.count++
    }, 1000)
  },
  beforeDestroy() {
    console.log('beforeDestroy')
    if (this.timer) {
      clearInterval(this.timer)
    }
  },
  methods: {
    fetchData() {
      // 请求数据
    }
  }
}
</script>

Vue3 Composition API 写法:

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

// setup 阶段(相当于 created)
const count = ref(0)
let timer = null

console.log('setup 阶段')
fetchData()

onMounted(() => {
  console.log('onMounted')
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

onBeforeUnmount(() => {
  console.log('onBeforeUnmount')
  if (timer) {
    clearInterval(timer)
  }
})

function fetchData() {
  // 请求数据
}
</script>
执行顺序
Vue3 Composition API 执行顺序:
  setup()
    ↓
  onBeforeMount()
    ↓
  onMounted()
    ↓
  onBeforeUpdate()  (数据变化时)
    ↓
  onUpdated()       (数据变化时)
    ↓
  onBeforeUnmount()
    ↓
  onUnmounted()

核心要点:

Vue3 将 beforeDestroy/destroyed 重命名为 beforeUnmount/unmounted。Composition API 中,setup() 替代了 beforeCreate 和 created,其他生命周期需要从 vue 导入并以 on 开头。


四、响应式原理

4.1 Vue2 和 Vue3 响应式原理对比

难度:⭐⭐⭐⭐⭐

问题描述

Vue2 和 Vue3 的响应式原理有什么区别?各自的优缺点是什么?

答案解析
Vue2 响应式原理(Object.defineProperty)

实现方式: 基于 ES5 的 Object.defineProperty 实现

工作流程:

  1. 初始化时:递归遍历 data 对象的所有属性
  2. 添加 getter:收集依赖(记录哪些组件使用了这个属性)
  3. 添加 setter:通知依赖更新(数据变化时通知相关组件重新渲染)

核心代码示例:

function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  observe(val)

  Object.defineProperty(obj, key, {
    get() {
      console.log(`访问了 ${key}`)
      // 收集依赖
      track(obj, key)
      return val
    },
    set(newVal) {
      if (newVal === val) return
      console.log(`修改了 ${key}`)
      val = newVal
      // 通知更新
      trigger(obj, key)
    }
  })
}

// 使用示例
const data = { count: 0 }
defineReactive(data, 'count', data.count)

data.count // 触发 getter
data.count = 1 // 触发 setter

局限性:

  1. 无法监听对象新增/删除属性
const data = { name: '张三' }
// ❌ 新增属性不会触发更新
data.age = 20

// ✅ 需要使用 Vue.set
Vue.set(data, 'age', 20)
// 或
this.$set(data, 'age', 20)
  1. 无法监听数组索引修改
const arr = [1, 2, 3]
// ❌ 通过索引修改不会触发更新
arr[0] = 100

// ✅ 需要使用数组方法
arr.splice(0, 1, 100)
// 或使用 Vue.set
Vue.set(arr, 0, 100)
  1. 初始化性能问题
  • 需要递归遍历所有属性
  • 大对象初始化时性能损耗明显
Vue3 响应式原理(Proxy)

实现方式: 基于 ES6 的 Proxy 实现

工作流程:

  1. 创建代理:用 Proxy 代理整个对象
  2. 拦截操作:拦截对象的所有操作(get、set、has、deleteProperty 等)
  3. 懒代理:只有访问属性时才递归处理嵌套对象

核心代码示例:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      console.log(`访问了 ${key}`)
      const result = Reflect.get(target, key, receiver)

      // 收集依赖
      track(target, key)

      // 懒代理:只有访问时才递归
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }

      return result
    },
    set(target, key, value, receiver) {
      console.log(`修改了 ${key}`)
      const result = Reflect.set(target, key, value, receiver)

      // 通知更新
      trigger(target, key)

      return result
    },
    deleteProperty(target, key) {
      console.log(`删除了 ${key}`)
      const result = Reflect.deleteProperty(target, key)

      // 通知更新
      trigger(target, key)

      return result
    }
  })
}

// 使用示例
const data = reactive({ count: 0 })

data.count // 触发 get
data.count = 1 // 触发 set
delete data.count // 触发 deleteProperty

优势:

  1. 原生支持新增/删除属性
const data = reactive({ name: '张三' })
// ✅ 新增属性会触发更新
data.age = 20
// ✅ 删除属性会触发更新
delete data.age
  1. 完美支持数组
const arr = reactive([1, 2, 3])
// ✅ 索引修改会触发更新
arr[0] = 100
// ✅ 长度修改会触发更新
arr.length = 0
  1. 性能更优
  • 懒代理:只有访问时才处理嵌套对象
  • 不需要初始化时递归遍历所有属性
  1. 支持更多数据结构
// ✅ 支持 Map
const map = reactive(new Map())
map.set('key', 'value')

// ✅ 支持 Set
const set = reactive(new Set())
set.add(1)
对比总结
对比项Vue2 (Object.defineProperty)Vue3 (Proxy)
新增属性❌ 不支持,需要 $set✅ 原生支持
删除属性❌ 不支持,需要 $delete✅ 原生支持
数组索引❌ 不支持✅ 原生支持
数组长度❌ 不支持✅ 原生支持
Map/Set❌ 不支持✅ 原生支持
初始化性能❌ 需要递归遍历✅ 懒代理,按需处理
浏览器兼容✅ IE9+❌ IE 不支持
实际使用对比

Vue2:

<script>
export default {
  data() {
    return {
      user: {
        name: '张三'
      },
      list: [1, 2, 3]
    }
  },
  methods: {
    addAge() {
      // ❌ 不会触发更新
      this.user.age = 20

      // ✅ 正确做法
      this.$set(this.user, 'age', 20)
    },
    updateList() {
      // ❌ 不会触发更新
      this.list[0] = 100

      // ✅ 正确做法
      this.$set(this.list, 0, 100)
      // 或
      this.list.splice(0, 1, 100)
    }
  }
}
</script>

Vue3:

<script setup>
import { reactive } from 'vue'

const user = reactive({
  name: '张三'
})

const list = reactive([1, 2, 3])

function addAge() {
  // ✅ 直接添加,会触发更新
  user.age = 20
}

function updateList() {
  // ✅ 直接修改,会触发更新
  list[0] = 100
}
</script>

核心要点:

Vue2 使用 Object.defineProperty 实现响应式,存在无法监听新增/删除属性和数组索引修改的问题。Vue3 使用 Proxy 实现,完美解决了这些问题,性能更优,但不支持 IE 浏览器。


五、Vue3 新特性

5.1 Vue2 和 Vue3 的核心区别

难度:⭐⭐⭐⭐

问题描述

Vue2 和 Vue3 有哪些核心区别?为什么要升级到 Vue3?

答案解析
1. 响应式机制不同

Vue2: Object.defineProperty

  • 无法监听对象新增/删除属性
  • 无法监听数组索引修改
  • 初始化时需要递归遍历所有属性

Vue3: Proxy

  • 原生支持对象新增/删除属性
  • 完美支持数组操作
  • 懒代理,性能更优
// Vue2
this.$set(this.user, 'age', 20) // 需要使用 $set

// Vue3
user.age = 20 // 直接添加即可
2. 代码组织方式

Vue2: Options API(选项式 API)

  • 代码按 data、methods、computed 等选项划分
  • 相关逻辑分散在不同选项中
  • 难以复用逻辑
<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() { this.count++ }
  },
  computed: {
    double() { return this.count * 2 }
  }
}
</script>

Vue3: Composition API(组合式 API)

  • 按业务逻辑组织代码
  • 相关逻辑聚合在一起
  • 易于复用和维护
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const increment = () => count.value++
const double = computed(() => count.value * 2)
</script>
3. 性能优化

编译优化:

  • 静态提升:静态节点只创建一次,后续复用
  • 事件缓存:事件处理函数缓存,避免重复创建
  • Patch Flag:标记动态节点,更新时只处理动态内容
<!-- Vue3 编译优化 -->
<template>
  <div>
    <p>静态文本</p>  <!-- 静态提升,只创建一次 -->
    <p>{{ count }}</p>  <!-- 标记为动态节点 -->
  </div>
</template>

体积优化:

  • Tree-shaking:未使用的 API 不会打包
  • 核心库体积减少约 40%
// Vue3 按需引入
import { ref, computed } from 'vue'
// 未使用的 watch、reactive 等不会打包

渲染优化:

  • Fragment:支持多根节点,减少不必要的 DOM 嵌套
  • Teleport:将组件渲染到 DOM 的其他位置
  • Suspense:异步组件加载状态管理
<!-- Vue3 支持多根节点 -->
<template>
  <header>头部</header>
  <main>内容</main>
  <footer>底部</footer>
</template>
4. TypeScript 支持

Vue2:

  • TypeScript 支持不完善
  • 需要额外配置

Vue3:

  • 源码用 TypeScript 重写
  • 类型定义更完善
  • 开发时类型提示更友好
// Vue3 TypeScript 支持
import { ref, Ref } from 'vue'

const count: Ref<number> = ref(0)
const message: Ref<string> = ref('Hello')
5. 新增特性

Composition API:

<script setup>
import { ref, reactive, computed, watch } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三' })
const double = computed(() => count.value * 2)

watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
</script>

Teleport(传送门):

<template>
  <!-- 将模态框渲染到 body 下 -->
  <Teleport to="body">
    <div class="modal">模态框内容</div>
  </Teleport>
</template>

Suspense(异步组件):

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>
6. 生命周期变化
Vue2Vue3 Options APIVue3 Composition API
beforeCreatebeforeCreatesetup()
createdcreatedsetup()
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted
7. 移除的特性

Vue3 移除了:

  • $on$off$once(事件总线)
  • filter(过滤器,推荐用计算属性或方法替代)
  • $children(推荐用 ref 访问子组件)
  • inline-template
  • $destroy
8. v-model 变化

Vue2:

  • 只能绑定一个 v-model
  • 默认绑定 value 属性和 input 事件

Vue3:

  • 支持多个 v-model
  • 默认绑定 modelValue 属性和 update:modelValue 事件
<!-- Vue3 多个 v-model -->
<template>
  <UserForm
    v-model:name="userName"
    v-model:age="userAge"
  />
</template>
核心优势总结
对比项Vue2Vue3
响应式Object.definePropertyProxy
代码组织Options APIComposition API
性能一般更快(编译优化、Tree-shaking)
体积较大减少 40%
TypeScript支持一般完善支持
多根节点❌ 不支持✅ 支持
Teleport❌ 无✅ 有
Suspense❌ 无✅ 有

核心要点:

Vue3 在响应式机制、性能、TypeScript 支持、代码组织等方面全面升级,推荐新项目使用 Vue3,老项目可根据需求逐步迁移。


5.2 Vue3 常用 Composition API

难度:⭐⭐⭐⭐⭐

问题描述

Vue3 Composition API 有哪些常用的 API?如何使用?

答案解析
1. 响应式 API

ref - 创建基本类型响应式数据

<script setup>
import { ref } from 'vue'

// 创建响应式数据
const count = ref(0)
const message = ref('Hello')

// 访问值(必须通过 .value)
console.log(count.value) // 0

// 修改值
count.value++
message.value = 'Hi'

// 在模板中自动解包,无需 .value
</script>

<template>
  <div>{{ count }}</div>  <!-- 自动解包 -->
  <div>{{ message }}</div>
</template>

reactive - 创建对象/数组响应式数据

<script setup>
import { reactive } from 'vue'

// 创建响应式对象
const user = reactive({
  name: '张三',
  age: 20,
  hobbies: ['读书', '运动']
})

// 直接访问和修改(无需 .value)
console.log(user.name) // '张三'
user.age = 21
user.hobbies.push('旅游')
</script>

<template>
  <div>{{ user.name }} - {{ user.age }}</div>
</template>

toRef / toRefs - 保持响应式关联

<script setup>
import { reactive, toRef, toRefs } from 'vue'

const user = reactive({
  name: '张三',
  age: 20
})

// toRef:转换单个属性
const ageRef = toRef(user, 'age')
ageRef.value = 21 // 会同步更新 user.age

// toRefs:转换所有属性(常用于解构)
const { name, age } = toRefs(user)
name.value = '李四' // 会同步更新 user.name
</script>

isRef / isReactive - 类型判断

import { ref, reactive, isRef, isReactive } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三' })

console.log(isRef(count)) // true
console.log(isReactive(user)) // true
2. 计算属性与监听

computed - 计算属性

<script setup>
import { ref, computed } from 'vue'

const count = ref(0)

// 只读计算属性
const double = computed(() => count.value * 2)

// 可写计算属性
const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
  get: () => `${firstName.value}${lastName.value}`,
  set: (val) => {
    const names = val.split('')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

fullName.value = '李四' // 触发 setter
</script>

watch - 监听数据变化

<script setup>
import { ref, reactive, watch } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三', age: 20 })

// 监听 ref
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})

// 监听 reactive 的某个属性
watch(() => user.age, (newVal, oldVal) => {
  console.log(`age 从 ${oldVal} 变为 ${newVal}`)
})

// 监听多个数据
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
  console.log(`count: ${newCount}, age: ${newAge}`)
})

// 深度监听
watch(user, (newVal) => {
  console.log('user 发生变化', newVal)
}, { deep: true })

// 立即执行
watch(count, (newVal) => {
  console.log('count:', newVal)
}, { immediate: true })
</script>

watchEffect - 自动追踪依赖

<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 自动追踪 count 和 message 的变化
watchEffect(() => {
  console.log(`count: ${count.value}, message: ${message.value}`)
})

// 停止监听
const stop = watchEffect(() => {
  console.log(count.value)
})

// 手动停止
stop()
</script>
3. 生命周期钩子
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

// 挂载前
onBeforeMount(() => {
  console.log('组件即将挂载')
})

// 挂载后
onMounted(() => {
  console.log('组件已挂载')
  // 适合:操作 DOM、发起请求、添加事件监听
})

// 更新前
onBeforeUpdate(() => {
  console.log('组件即将更新')
})

// 更新后
onUpdated(() => {
  console.log('组件已更新')
})

// 卸载前
onBeforeUnmount(() => {
  console.log('组件即将卸载')
  // 适合:清除定时器、移除事件监听
})

// 卸载后
onUnmounted(() => {
  console.log('组件已卸载')
})
</script>
4. 依赖注入

provide / inject - 跨层级传递数据

<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const updateTheme = (newTheme) => {
  theme.value = newTheme
}

// 提供数据
provide('theme', theme)
provide('updateTheme', updateTheme)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

// 接收数据(第二个参数为默认值)
const theme = inject('theme', 'light')
const updateTheme = inject('updateTheme')

// 使用
updateTheme('light')
</script>
5. 其他常用 API

getCurrentInstance - 获取组件实例

<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

// 访问 $refs、$emit 等
console.log(instance.refs)
console.log(instance.emit)
</script>

nextTick - 等待 DOM 更新

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

const updateCount = async () => {
  count.value++

  // 等待 DOM 更新完成
  await nextTick()

  // 此时 DOM 已更新
  console.log('DOM 已更新')
}
</script>

defineProps / defineEmits - 组件通信

<script setup>
import { defineProps, defineEmits } from 'vue'

// 定义 props
const props = defineProps({
  message: String,
  count: {
    type: Number,
    default: 0
  }
})

// 定义 emits
const emit = defineEmits(['update', 'delete'])

// 触发事件
const handleUpdate = () => {
  emit('update', props.count + 1)
}
</script>

defineExpose - 暴露给父组件

<script setup>
import { ref, defineExpose } from 'vue'

const count = ref(0)
const increment = () => count.value++

// 暴露给父组件
defineExpose({
  count,
  increment
})
</script>
常用 API 总结
分类API用途
响应式ref基本类型响应式
reactive对象/数组响应式
toRef / toRefs保持响应式关联
计算与监听computed计算属性
watch监听数据变化
watchEffect自动追踪依赖
生命周期onMounted挂载后
onBeforeUnmount卸载前
依赖注入provide / inject跨层级传递数据
组件通信defineProps接收 props
defineEmits触发事件
defineExpose暴露给父组件

核心要点:

Composition API 提供了更灵活的代码组织方式,核心是 ref/reactive 创建响应式数据,computed/watch 处理逻辑,生命周期钩子管理副作用。


六、路由相关

6.1 Hash 模式和 History 模式的区别

难度:⭐⭐⭐

问题描述

Vue Router 的 Hash 模式和 History 模式有什么区别?各自的优缺点是什么?

答案解析
一、URL 形态

Hash 模式:

  • URL 中包含 # 符号
  • # 后的内容为前端路由,不会发送到服务器
http://example.com/#/home
http://example.com/#/user/123
http://example.com/#/about

History 模式:

  • URL 无 #,形态更接近传统 URL
  • 更美观,更符合用户习惯
http://example.com/home
http://example.com/user/123
http://example.com/about
二、实现原理

Hash 模式:

  • 基于 location.hashhashchange 事件
  • 监听 URL 中 # 后的变化更新页面内容
  • # 后的内容不会发送到服务器
// Hash 模式原理
window.addEventListener('hashchange', () => {
  const hash = location.hash.slice(1) // 去掉 #
  console.log('路由变化:', hash)
  // 根据 hash 渲染对应组件
})

// 改变路由
location.hash = '/home'

History 模式:

  • 基于 HTML5 History API(pushStatereplaceState
  • 通过 popstate 事件监听路由变化
  • 修改浏览器历史记录栈,不刷新页面
// History 模式原理
window.addEventListener('popstate', (event) => {
  console.log('路由变化:', location.pathname)
  // 根据 pathname 渲染对应组件
})

// 改变路由(不刷新页面)
history.pushState({}, '', '/home')
history.replaceState({}, '', '/about')
三、服务端配置

Hash 模式:

  • 无需服务端配置
  • 直接部署即可正常使用
  • # 后的内容不会发送到服务器,服务器始终返回 index.html

History 模式:

  • 需要服务端支持
  • 直接访问子路由会返回 404 错误
  • 需要配置服务器,将所有路由重定向到 index.html

Nginx 配置示例:

server {
  listen 80;
  server_name example.com;
  root /var/www/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

Apache 配置示例:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Node.js (Express) 配置示例:

const express = require('express')
const path = require('path')
const app = express()

app.use(express.static(path.join(__dirname, 'dist')))

// 所有路由都返回 index.html
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})

app.listen(3000)
四、浏览器兼容性

Hash 模式:

  • ✅ 兼容所有浏览器(包括 IE8+)
  • 基于 URL hash,所有浏览器都支持

History 模式:

  • ❌ 依赖 HTML5 History API
  • 仅支持 IE10+ 及现代浏览器
  • 不支持 IE9 及以下版本
五、SEO 优化

Hash 模式:

  • ❌ 对 SEO 不友好
  • # 后的内容可能被搜索引擎忽略
  • 搜索引擎可能将所有页面视为同一个页面

History 模式:

  • ✅ 对 SEO 更友好
  • 完整 URL 更易被搜索引擎抓取
  • 但仍需配合服务端渲染(SSR)实现完整 SEO 优化
六、Vue Router 配置

Hash 模式(默认):

// Vue Router 3 (Vue2)
import VueRouter from 'vue-router'

const router = new VueRouter({
  mode: 'hash', // 默认值
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
})
// Vue Router 4 (Vue3)
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
})

History 模式:

// Vue Router 3 (Vue2)
import VueRouter from 'vue-router'

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
})
// Vue Router 4 (Vue3)
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
})
七、适用场景

选择 Hash 模式:

  • ✅ 对 SEO 无要求的应用
  • ✅ 需要兼容老旧浏览器
  • ✅ 无法配置服务器的场景
  • ✅ 快速开发和部署
  • ✅ 内部管理系统、后台系统

选择 History 模式:

  • ✅ 追求 URL 美观性
  • ✅ 需要 SEO 优化的项目
  • ✅ 具备服务端配置能力
  • ✅ 面向用户的网站
  • ✅ 现代浏览器环境
八、对比总结
对比项Hash 模式History 模式
URL 形态#/home/home
实现原理hashchange 事件History API
服务端配置✅ 无需配置❌ 需要配置
浏览器兼容✅ IE8+❌ IE10+
SEO 优化❌ 不友好✅ 更友好
URL 美观❌ 有 # 符号✅ 无 # 符号
部署难度✅ 简单❌ 需要服务器支持
适用场景内部系统、快速开发面向用户的网站
九、常见问题

1. History 模式刷新 404 问题

问题: 直接访问 http://example.com/user/123 返回 404

原因: 服务器找不到 /user/123 对应的文件

解决: 配置服务器,将所有路由重定向到 index.html

2. Hash 模式锚点冲突

问题: 页面内锚点跳转与路由冲突

解决: 使用 scrollBehavior 配置

const router = new VueRouter({
  mode: 'hash',
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      return { selector: to.hash }
    }
  }
})

核心要点:

Hash 模式简化了部署和维护,但牺牲了 URL 规范性和 SEO;History 模式提供了更专业的用户体验,但需额外服务端支持。开发者应根据项目需求权衡选择。


总结

本文档系统整理了 Vue.js 的核心面试题,涵盖了从基础概念到高级特性的各个方面:

  1. 基础概念:构建工具、MVVM/MVC、指令、data 函数、ref 使用
  2. 组件通信:Props/Emit、Vuex、Pinia、Provide/Inject
  3. 生命周期:Vue2/3 生命周期对比、使用场景
  4. 响应式原理:Object.defineProperty vs Proxy
  5. Vue3 新特性:Composition API、性能优化、新增特性
  6. 路由相关:Hash 模式 vs History 模式

掌握这些知识点,能够帮助你在 Vue.js 面试中脱颖而出,也能在实际项目开发中游刃有余。

学习建议:

  • 理解原理比记忆 API 更重要
  • 多动手实践,加深理解
  • 关注 Vue 官方文档的更新
  • 结合实际项目场景学习

祝你面试顺利!🎉

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐