Vue面试题
MVVM模型
MVVM 模型其实就是一种让代码分工更明确的开发模式,主要分三个部分:
Model(数据模型),对应data()
返回的对象 + Pinia/Vuex状态。负责存数据,和处理数据逻辑。比如从数据库读写、网络请求这些
View(视图层),对应模板语法(<template>
中的 HTML),只负责显示数据和接收用户操作。不处理数据逻辑;
ViewModel(视图模型),是Vue 实例本身(处理数据绑定/事件监听),一个中间层,相当于数据和界面之间的桥梁
双向绑定原理(ViewModel 的核心)
- 模板编译:将模板解析为 Render 函数
- 依赖收集:渲染时触发 getter 记录依赖关系
- 派发更新:数据变更时通知关联组件更新
vue实例对象前端主流的框架都是这个思想:把数据放在要求的位置(Model),写出模板代码(view)。里面具体怎么插入值中间的vm实例对象来操作
最核心的优点:
前端程序员可以专注写界面,后端程序员专注处理数据,两边互不干扰
界面变化不会影响数据逻辑,数据逻辑改了也不用动界面代码
并且方便写单元测试,因为ViewModel可以独立测试
Vue2的数据代理
核心目标:让我们在组件中能用this.xxx直接访问/修改 data 中的属性,而不是繁琐的this._data.xxxObject.defineProperty
是JavaScript提供的一个底层API,它允许精确地定义或修改对象上的属性,特别是能定义getter/setter实现访问器属性。Vue2的核心响应式系统正是建立在这个API之上,它通过两步走:
- 首先创建 _data 存储原始数据
Vue会执行 data() 函数,得到原始数据对象,直接保存到vm._data属性上 - 第二步进行数据劫持 (Observer):使用
Object.defineProperty
递归地将保存在 vm._data 上的属性转换成 getter/setter,用于实现响应式。
(在getter中收集依赖(记录谁在用这个数据),在setter中通知更新(告诉依赖者数据变了)。) - 第三步进行数据代理:同样使用 Object.defineProperty,在vm上为_data的每个顶层属性,创建一层代理。当我们访问或修改
vm.xxx
时,实际上是通过代理的getter/setter
去访问或修改vm._data.xxx
。
这层代理让我们能直接用this.
操作数据,语法更简洁自然。
局限
无法检测对象属性的新增 / 删除;无法监听数组索引和长度变化
Object.defineProperty
Object.defineProperty是JavaScript 提供的一个底层API,它允许精确地定义或修改对象上的属性
1 | let person={ |
- Object.defineProperty 添加的属性默认不可枚举,默认不会出现在 for…in 循环或 Object.keys() 的结果里
解决:显式设置enumerable: true
- Object.defineProperty 添加的属性值默认不可以修改
解决:writabe:true
- Object.defineProperty 添加的属性不可以随意删除
解决: 设置configurable: true
- get和set访问器属性
xxx属性本身并不存储值。它的值是通过get函数动态返回的(这里是 _xxx 的值)。当给它赋值时,是通过set函数去修改关联的_xxx变量
实现步骤
- 存储原始数据 (_data):Vue 在创建组件实例时,会执行我们定义的 data() 函数,得到原始数据对象,并将它直接保存到实例的 _data 属性上。此时,原始数据在 vm._data上
- 代理到实例 (vm):Vue会遍历data对象的所有顶层属性,对于每一个属性key,使用Object.defineProperty在组件实例本身上定义一个同名属性
- 定义代理的 get/set当写this.name或
1
2
3
4
5
6
7
8
9
10Object.defineProperty(vm, key, {
get() {
return vm._data[key]; //访问 this.key 实际上是访问 this._data.key
},
set(newValue) {
vm._data[key] = newValue; //修改 this.key 实际上是修改 this._data.key
},
enumerable: true,
configurable: true
});{{ name }}
时:
读取 (get):触发定义在 vm 上的 name 属性的 getter,它返回 vm._data.name 的值。
修改 (set):触发定义在 vm 上的 name 属性的 setter,它将新值赋给 vm._data.name
_data?
_data是Vue内部存储原始响应式数据的地方
vue3的数据代理和数据劫持
代理与劫持合一:reactive()
返回的Proxy对象同时实现了对整个对象的代理和劫持
无中间层:直接操作代理对象(无 _data
概念)
精准更新:通过 track
/trigger
实现属性级依赖追踪
解决:
解决历史痛点: 完美支持了数组索引/长度修改和对象属性的动态增删,开发者可以直接使用原生 JS 语法操作数组和对象,彻底告别 Vue.set 和 Vue.delete。这极大简化了开发心智模型。
性能与精度提升: 通过 惰性转换 减少初始化开销,并通过 Proxy 的能力实现了 更细粒度的依赖追踪,只在真正访问到的属性变化时才触发更新,优化了运行效率。
ref 的定位: ref 主要用于处理基本类型值和需要保持稳定引用的场景,通过 .value 访问其值。在模板中会自动解包。reactive 则专注于处理对象和数组。
核心思想:Proxy取代 Object.defineProperty
Vue3的响应式系统永强大的原生 JavaScript 特性:Proxy 和 Reflect
- Proxy是什么
它允许你创建一个对象的代理(Proxy)。这个代理可以拦截并重新定义对该对象的基本操作(如属性读取、设置、删除、方法调用等)。
它代理的是整个对象,而不是像 Object.defineProperty 那样需要递归遍历并逐个定义对象的属性。
拦截操作范围更广: 可以拦截 get, set, deleteProperty, has, ownKeys 等十多种操作。这是实现更完善响应式的关键。
2. Vue 3 如何利用 Proxy 实现响应式?
Vue 3 主要通过两个核心函数:reactive() 和 ref()。
reactive(obj): 这是 Vue 3 处理对象 (Object) 和数组 (Array) 响应式的主力。
1 | import { reactive } from 'vue'; |
内部过程: reactive 函数接收一个普通对象,然后返回这个对象的 Proxy 代理。
劫持与代理合一: 这个 Proxy 同时完成了 Vue 2 中 数据代理(方便访问) 和 数据劫持(追踪依赖、触发更新) 的工作
解决Vue2的痛点
- 完美支持数组索引和变更方法:
Vue 2 问题: 无法检测通过索引直接设置项 (arr[index] = newValue) 或修改数组长度 (arr.length = 0)。需要特殊处理数组的 push, pop, shift, unshift, splice, sort, reverse 方法。
Vue 3 解决: Proxy 的 set 陷阱可以完美拦截 arr[index] = newValue 和 arr.length = newLength 操作。数组的内置方法(如 push)内部会执行索引设置或 length 修改,自然会被 Proxy 捕获。不再需要特殊 hack 数组方法! - 完美支持对象属性的动态添加和删除:
- 更细粒度的依赖追踪:
Vue2 追踪的依赖是对象属性的 getter。嵌套对象需要递归遍历,初始化开销大 - 更统一简洁的实现:
Vue 2: 需要区分数据代理 (vm 属性代理 _data) 和数据劫持 (Observer 处理 _data)。实现相对复杂。
Vue 3: reactive() 返回的 Proxy 代理一步到位
Options API
Vue的OptionsAPI,我的理解是:它是 Vue2的默认组件组织方式,也是 Vue 3 完全兼容的经典模式。它以声明式的“选项”对象为核心,将组件的不同功能逻辑,划分到预定义好的“格子”里,核心思想是按功能类型划分代码
痛点:逻辑分散:同一功能的代码分散在 data/methods/computed/watch中,复用困难:混入(mixins)导致命名冲突和隐式依赖
核心思想:按功能类型划分代码
1 | // 典型的 Options API 组件 |
data():定义组件的本地响应式状态
为什么必须是函数:组件会被复用,如果 data 是对象,所有实例共享同一份数据(引用类型浅拷贝问题)
Vue遍历 data 对象的所有属性,vue2中用Object.defineProperty 添加 getter/setter
props用来声明组件接收的外部传入的属性
computed计算属性:定义基于响应式状态的衍生值
计算结果会被缓存,只有依赖的响应式状态变化时才会重新计算。
声明: 函数形式 (computed: { fullName() { return this.firstName + ‘ ‘ + this.lastName; } })。
使用: 像普通属性一样使用 ({{ fullName }}
或 this.fullName)
methods方法:定义可以在模板,或组件逻辑中调用的函数。用于事件处理、业务逻辑封装。
声明: 普通函数 (methods: { handleClick() { … } })。
this 绑定: Vue 自动将 methods 中的函数绑定到组件实例 (this),避免丢失上下文。
注意: 不要使用箭头函数定义方法,否则this不会指向组件实例。
箭头函数没有自己的this,会继承父级作用域的this,指向window无法被Vue重重新绑定
watch:在响应式状态发生变化时执行
默认只监听引用变化,对象内部属性变化不会触发
可以侦听 data/props/computed 属性。支持深度侦听 (deep: true)、立即执行 (immediate: true)、回调接收新旧值。
生命周期钩子 (如 created, mounted, updated, beforeUnmount 等)
Composition API
将同一功能的代码放在一个setup()函数中,同一功能的状态/方法/生命周期可以集中书写
灵活复用:通过自定义hook复用逻辑替代 Mixins(解决命名冲突)
类型支持:更好的 TypeScript 集成(setup 函数返回明确类型
Vue 2和Vue3之间的不同
Vue 3 是全面进化的版本
- 响应式系统
Vue 2: 基于Object.defineProperty实现响应,需递归遍历+单独处理数组式数据绑定
Vue 3: 用Proxy实现对象级监听
vue3的核心技术是Proxy + Reflect,代理整个对象,无需递归初始化属性
vue3可以直接进行数组索引修改;动态增删属性 - API设计
Vue 2: 主要采用Options API,按功能类型分块,比如(如data, methods, computed等)选项
Vue 3: Composition API, 允许将同一功能的状态/方法/生命周期集中书写,解决逻辑复用 - 生命周期钩子
Vue 2: 提供了一系列生命周期钩子,比如beforeCreate, created, beforeMount, mounted等。
Vue 3: 精简并重组了这些钩子,例如beforeDestroy变成了onBeforeUnmount,destroyed变为onUnmounted,beforeCreate被setup() 取代 - 性能优化
编译时优化:PatchFlag标记动态节点,Diff效率提升
虚拟DOM优化、按需编译、静态提升、事件缓存、Proxy 响应式系统、Tree-shaking 等按需编译:Vue3 的编译器会分析模板,只生成需要的代码(如静态节点不再重复渲染)
静态内容(如纯文本)只创建一次,不再参与 diff 计算
内联事件处理函数会被缓存,避免每次渲染都创建新函数
打包体积:Vue2全量引入,Vue3Tree-shaking 按需引入 - 生态系统
Vue3 推荐使用Vite 构建工具;Pinia状态管理库,对TypeScript支持更好,API更简洁;VueUse,基于 Composition API 的工具库提供大量可复用的hook
生命周期
Vue3 的生命周期是指组件从创建到销毁过程中的一系列阶段,每个阶段都有对应的生命周期钩子函数,让开发者可以在特定阶段执行相应的逻辑
组件创建阶段
setup:在组件创建之前执行,用于初始化组件的响应式数据、计算属性、方法等,是 Composition API 的入口点。比如在setup中可以定义组件的初始状态数据const count = ref(0)。
onBeforeMount:在组件挂载到 DOM 之前调用。此时组件的render函数已经被调用,但还没有实际的 DOM 节点被创建。可以在这个钩子中进行一些数据的最后准备工作,比如对即将渲染的数据进行最后的格式化。
onMounted:组件挂载到 DOM 后调用。此时可以访问到真实的 DOM 节点,常用于发送网络请求获取数据,然后更新组件状态来渲染数据到页面上,或者进行一些需要 DOM 节点的初始化操作,如echarts图表的初始化。
组件更新阶段
onBeforeUpdate:在组件更新之前调用,此时组件的响应式数据已经发生了变化,但 DOM 还没有更新。可以在这个钩子中做一些数据更新前的准备工作,比如保存旧的数据状态。
onUpdated:组件更新完成后调用,此时 DOM 已经根据最新的数据进行了更新。可以在这个钩子中执行一些依赖于更新后 DOM 的操作,比如操作更新后的 DOM 元素,或者根据新的 DOM 状态重新计算一些布局相关的值。
组件销毁阶段
onBeforeUnmount:在组件卸载之前调用。可以在这个钩子中进行一些清理工作,比如清除定时器、取消网络请求、解绑全局事件等,以避免内存泄漏。
onUnmounted:组件卸载后调用。此时组件实例以及相关的 DOM 都已经被销毁,组件相关的所有资源都应该被清理干净。
其他钩子
onErrorCaptured:当组件或其子组件发生错误时会被调用。可以用于捕获错误并进行统一的错误处理,比如记录错误信息、展示错误提示给用户等,有助于提高应用的稳定性和可维护性。
Options API 生命周期钩子是预定义的组件选项,与 data、methods 并列。它们是 “被动声明” 的,由 Vue 在特定时刻调用。
Composition API :生命周期钩子是可注册的函数,在setUp中主动调用
Vue 3 setup() 取代了 beforeCreate/created,成为组合式逻辑的入口;
钩子变为可注册的函数 (onXxx),支持在功能逻辑块内就近多次注册
命名更语义化,on开头
解决了Options API 生命周期逻辑分散的痛点,实现功能内聚
组件通信
父子组件:props + emit(如表单控件)
祖孙层级深:provide/inject(如主题/语言)
全局状态:Pinia(如用户登录状态)
兄弟组件:提升状态到父级 或 用 Pinia
特殊需求:$refs(调用子组件)/$attrs(透传) → Vue 3合并$attrs
Vue 3 黄金法则:能用 provide 不用事件总线,能用 Pinia 不用 Vuex
父子通信
父传子:Props
父组件传递数据:在父组件的模板中,通过在子组件标签上以属性名=“属性值”的形式来传递数据。如<Child :msg="parentMsg" />
,这里parentMsg是父组件中定义的响应式数据,:是 Vue 的指令语法,用于将parentMsg的值绑定到子组件的msg属性上。
子组件接收数据:在子组件中,使用defineProps宏来声明接收父组件传递的Props。如const props = defineProps(['msg']);
,这样就可以在子组件中通过props.msg来访问父组件传递过来的数据了。
子传父:自定义事件
子组件触发事件:子组件通过defineEmits宏来定义可以触发的自定义事件。如const emit = defineEmits([‘handle - event’]);,然后在子组件的方法中使用emit函数来触发事件,如**emit(‘handle - event’, ‘子组件的数据’);**,这里第一个参数是事件名,第二个参数是要传递给父组件的数据。
父组件监听事件:在父组件的子组件标签上使用@事件名=“事件处理函数”的形式来监听子组件触发的事件。如<Child @handle - event="handleChildEvent" />
,handleChildEvent是父组件中定义的方法,当子组件触发handle - event事件时,父组件的handleChildEvent方法就会被调用,并且可以接收到子组件传递过来的数据。
子组件接收数据:
通过 defineProps 声明 Props,支持两种方式:
数组形式:defineProps([‘msg’, ‘count’, ‘user’])(适用于简单类型)。
对象形式:可指定类型、必填项、默认值(如 msg: { type: String, required: true }),用于类型校验和默认值设置。
代码例子
- 父子组件通信:
事件上传:Vue 2this.$emit('event', data)
,vue3defineEmits + emit('event')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40<template>
<!-- 1. 在子组件标签上用 `:` 绑定数据(父传子) -->
<Child
:msg="parentMsg" <!-- 传递字符串数据 -->
:count="counter" <!-- 传递响应式数值 -->
:user="currentUser" <!-- 传递对象数据 -->
@child-event="handleChildEvent" <!-- 监听子组件事件 -->
/>
<!-- 父组件自身内容 -->
<div>
<p>父组件计数器:{{ counter }}</p>
<button @click="incrementCounter">+1 父组件计数器</button>
</div>
</template>
<script setup>
// 引入 Vue 响应式工具
import { ref, reactive } from 'vue';
// 1. 定义父组件数据
const parentMsg = '这是父组件传递的字符串'; // 普通字符串
const counter = ref(0); // 响应式数值(修改后视图自动更新)
const currentUser = reactive({ // 响应式对象
name: 'Alice',
age: 25
});
// 2. 定义处理子组件事件的函数
const handleChildEvent = (data) => {
console.log('子组件传递的数据:', data); // 控制台打印子组件数据
// 父组件可根据子组件数据做逻辑处理(示例:更新计数器)
counter.value += 1;
};
// 3. 父组件自有方法(示例:修改计数器)
const incrementCounter = () => {
counter.value += 1;
};
</script>
1 | <template> |
- 跨层级通信Provide/Inject
Vue 2:选项式:provide: { key: val } inject: [‘key’]
Vue 3:函数式:provide(key, value) inject(key)
事件总线:
Vue2: new Vue() 作为 EventBus
Vue3:废弃EventBus → 改用 mitt库1
2
3
4
5
6// Vue 3 Provide 响应式方案
import { provide, ref } from 'vue'
setup() {
const count = ref(0)
provide('count', count) // 子孙组件注入的 count 是响应式的
} - 全局状态管理
1
2
3
4
5
6
7
8export const useStore = defineStore('main', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++ // 直接修改(无需 mutations)
}
}
})
虚拟 DOM与Diff 算法
为什么用虚拟DOM:性能开销大,代码维护难
理解:真实DOM结构复杂,每次修改都会触发浏览器的重排和重绘,频繁直接操作 DOM 会导致代码混乱,难以维护。
目的:高效地更新DOM
对于简单场景,直接操作DOM可能更快。虚拟 DOM 的优势在于复杂视图频繁更新时的性能优化。
虚拟DOM的核心流程分为三步:生成 → 对比(diff) → 更新
虚拟DOM(主流框架的默认选择)
虚拟DOM是一种用JavaScript对象来模拟真实DOM树的技术。它是对真实 DOM 的抽象,以更高效的方式来更新和操作DOM,是一种轻量级的 JavaScript 对象
每个虚拟DOM节点对应一个真实DOM节点,但不包含真实DOM的所有属性(如事件监听、样式计算等)
一个虚拟DOM节点通常包含:标签名,属性,子节点
虚拟DOM节点的基本结构
1 | { |
虚拟DOM一定比直接操作DOM快吗
性能对比,三个角度考虑:
- 单次更新场景,真实DOM好
真实 DOM:直接操作(如dom.innerHTML = ‘新内容’),性能开销极低
虚拟 DOM:需经历「生成虚拟节点→Diff 对比→更新真实 DOM」流程,存在额外计算开销 - 频繁更新场景,虚拟DOM好
真实 DOM:每次更新都触发浏览器重排重绘,若更新频率极高(如每秒 100 次),可能导致卡顿
虚拟 DOM:通过批量更新和 Diff 算法,将多次更新合并为一次真实 DOM 操作,减少浏览器渲染压力 - 看节点复杂度
若唯一节点包含复杂事件绑定或嵌套结构,虚拟 DOM 的抽象层仍能提供统一的更新逻辑;若只是简单文本节点,真实 DOM 更轻量。
虚拟DOM应用场景:数据频繁更新的应用(如实时图表、聊天界面),复杂 UI 交互(如拖拽、动态表单),跨平台开发(React Native、Vue Weex)
开发与维护成本:
- 开发效率
真实DOM:直接操作API,代码更直观,但需手动处理所有细节(如事件绑定、属性更新)。
虚拟DOM:依赖框架(如 React/Vue),通过声明式编程描述 UI,代码更简洁(如{ count: 10 }自动映射为 DOM 内容) - 维护成本
真实 DOM:当逻辑复杂时(如条件渲染、动画交互),代码易变得碎片化,难以维护。
虚拟 DOM:框架提供的组件化机制可将逻辑封装,便于复用和调试(如 React 的组件生命周期、Vue 的响应式系统)
diff算法
找出新旧虚拟 DOM 树的差异,最小化真实 DOM 操作
静态提升:纯静态节点只创建一次,不再参与 diff
需要用到Key属性,key用于标识列表中的节点,帮助 Diff 算法快速识别哪些元素被添加、删除或移动
现代 Diff 算法的优化策略
分层比较:只比较同一层级的节点,不跨层级比较。
(如果发现节点不存在,直接删除整个子树,不会继续比较子节点)
相同类型节点:
如果节点类型相同(如都是div),则只更新属性和内容,保留子节点继续比较。
列表比较的 Key:
为列表项提供唯一的key,帮助算法识别哪些元素被添加、删除或移动。
组件比较:
同一类型的组件,保留实例并更新状态;不同类型的组件,直接替换。
计算属性和监听器
计算属性
它的值是根据其他响应式数据计算出来的。它会自动追踪依赖的响应式数据,只有这些依赖数据变化时,才会重新计算。而且计算属性是有缓存的,如果依赖的数据没有变化,多次访问计算属性会直接返回缓存的值,而不会重新计算
监听器
专门负责观察某个或某些响应式数据的变化。一旦被监听的数据发生了改变,就会触发相应的回调函数,你可以在回调函数中执行任何你需要的操作,比如发送网络请求、更新其他相关数据、记录日志等。
比如:假设要根据用户输入的搜索关键词来进行实时搜索,并在控制台打印出搜索结果。
如何实现 Vue Router 的懒加载?
可以通过动态导入的方式实现懒加载
在路由配置中写成component: () => import('../views/Home.vue')
这样做的好处是按需加载,提升首屏加载速度,避免一开始就加载全部组件
什么是组件化开发?为什么要用组件化
组件化开发就是把页面拆分成一个个独立、可复用的小模块,每个模块负责自己的功能。
这样做可以让代码结构更清晰,提高可维护性,还能提高开发效率,因为组件可以重复使用。
其他高频问题(简答版)
Q1:如何在 Vue3 中使用 ref 和 reactive?它们的区别是什么?
ref 用于创建一个响应式的基本类型或对象引用。
reactive 用于创建一个响应式的对象。
1 | import { ref, reactive } from 'vue'; |
Q2:<script setup>
是什么<script setup>
是一种编译时语法糖,可以在单文件组件(SFC)中更简洁地使用 Composition API。
所有在