1. MVC 和 MVVM 的区别
题目重述
请说明 MVC 与 MVVM 架构模式的区别,并解释 Vue 中为何未完全遵循 MVVM。
详解
MVC(Model-View-Controller) 是一种经典软件设计模式:
-
Model:处理数据逻辑,如数据库操作。
-
View:负责展示数据。
-
Controller:接收用户输入,协调 Model 和 View。
其核心思想是:Controller 将 Model 数据渲染到 View 上,即在控制器中完成数据赋值。
MVVM(Model-View-ViewModel) 在此基础上引入了 ViewModel 层:
-
ViewModel 实现双向绑定:
-
将 Model 转为 View 显示(数据绑定)
-
将 View 操作转为 Model 更新(DOM事件监听)
-
最大区别在于:MVVM 实现了 View 与 Model 的自动同步。当 Model 变化时,View 自动更新,无需手动操作 DOM ------ 这正是 Vue "数据驱动" 的体现。
但官方声明:Vue 并未完全遵循 MVVM 。
原因在于,严格的 MVVM 禁止 View 与 Model 直接通信,而 Vue 提供了 $refs 可让 Model 直接访问并操作 DOM 元素(View),违反了该原则。
知识点
-
MVVM 架构模式:通过 ViewModel 实现视图与模型的双向绑定,提升开发效率。
-
数据驱动视图:状态变化自动触发 UI 更新,无需手动操作 DOM。
-
$refs 的作用与限制:提供对 DOM 或子组件实例的直接引用,破坏了 MVVM 的隔离性。
2. 为什么 data 必须是一个函数?
题目重述
在 Vue 组件中,data 为什么要写成返回对象的函数形式?
详解
在 Vue 组件中,多个实例可能复用同一个组件模板。若 data 写为对象:
javascript
data: { count: 0 }
则所有组件实例将共享同一份 data 引用,造成"一处修改,处处响应"的问题。
而将其定义为函数:
javascript
data() {
return { count: 0 };
}
每次创建新实例时都会调用该函数,返回一个全新的独立数据对象,相当于为每个组件创建私有数据空间,避免相互影响。
这是 JavaScript 原型链机制下的必要设计。
知识点
-
组件实例隔离:确保各组件拥有独立的数据副本。
-
函数返回对象:实现数据私有化,防止引用共享。
-
JavaScript 闭包应用:利用函数作用域封装内部变量。
3. Vue 组件通讯方式有哪些?
题目重述
列举 Vue 中父子组件、兄弟组件之间的通信方式。
详解
常见的组件通信方式包括:
| 方式 | 说明 |
|---|---|
props / $emit |
父传子(props)、子传父($emit)------最基础的方式 |
$parent / $children |
访问父/子组件实例(不推荐深层依赖) |
$attrs / $listeners |
向下透传未被 props 接收的属性和事件(Vue 2.4+) |
provide / inject |
跨层级注入数据(适用于高阶组件或库开发) |
$refs |
获取子组件或 DOM 实例,进行直接调用 |
eventBus(事件总线) |
兄弟组件间通信(小型项目可用,大型建议用 Vuex) |
Vuex / Pinia |
全局状态管理,适用于复杂状态流 |
⚠️ 注意:Vue 3 推荐使用
mitt或emitter替代 EventBus;Vuex 已逐步被 Pinia 取代。
知识点
-
单向数据流原则:父组件通过 props 向下传递数据。
-
事件发射与监听:emit 触发事件,on/$off 监听(eventBus)。
-
跨级依赖注入:provide/inject 实现祖先向后代传值。
4. Vue 生命周期钩子有哪些?一般在哪一步发送请求?
题目重述
列出 Vue 的生命周期钩子函数,并说明异步请求的最佳发起时机。
详解
Vue 实例的完整生命周期如下:
| 钩子 | 执行时机 | 是否可访问 DOM | 是否支持 SSR |
|---|---|---|---|
beforeCreate |
实例初始化后,数据观测前 | ❌ | ✅ |
created |
实例创建完成,数据已响应式处理 | ❌(无 $el) | ✅ |
beforeMount |
挂载开始前,render 首次执行 | ❌ | ✅ |
mounted |
挂载完成后,可访问真实 DOM | ✅ | ❌(客户端) |
beforeUpdate |
数据更新前,patch 之前 | ✅ | ❌ |
updated |
虚拟 DOM 重新渲染后 | ✅ | ❌ |
beforeDestroy |
实例销毁前 | ✅ | ❌ |
destroyed |
实例销毁后 | ❌ | ❌ |
activated |
keep-alive 组件激活时 | ✅ | ❌ |
deactivated |
keep-alive 组件失活时 | ✅ | ❌ |
异步请求最佳位置:created
理由如下:
-
数据已初始化,可以安全赋值;
-
比
mounted更早获取数据,减少页面 loading 时间; -
支持服务端渲染(SSR),
beforeMount和mounted在 SSR 中不会执行。
✅ 推荐做法:
javascript
created() {
this.fetchData();
}
知识点
-
created 钩子用途:适合发起网络请求、初始化数据。
-
mounted 钩子用途:适合操作真实 DOM、绑定第三方插件。
-
SSR 兼容性差异:created 是唯一可在服务端运行的数据请求钩子。
5. v-if 与 v-show 的区别
题目重述
比较 v-if 与 v-show 的实现机制与使用场景。
详解
| 特性 | v-if |
v-show |
|---|---|---|
| 编译结果 | 条件不满足时不生成节点(移除 DOM) | 总是渲染,通过 display:none 控制显示 |
| 切换开销 | 高(需重建 VNode) | 低(仅样式切换) |
| 初始渲染开销 | 低(懒加载) | 高(始终渲染) |
| 适用场景 | 条件少变、初始不显示 | 频繁切换 |
示例:
html
<!-- v-if:条件 false 时不渲染 -->
<div v-if="visible">Hello</div>
<!-- v-show:总是存在,仅控制 display -->
<div v-show="visible">Hello</div>
扩展:display:none vs visibility:hidden vs opacity:0
| 属性 | 占据空间 | 子元素继承 | 触发事件 | 支持过渡动画 |
|---|---|---|---|---|
display: none |
否 | 否 | 否 | 否 |
visibility: hidden |
是 | 是 | 否 | 否 |
opacity: 0 |
是 | 是 | 是 | 是 |
知识点
-
条件渲染策略选择:根据频率决定用 v-if 或 v-show。
-
CSS 显示控制差异:理解不同隐藏方式的影响范围。
-
过渡动画兼容性:只有 opacity 支持 transition 动画。
6. Vue 内置指令有哪些?
题目重述
列举常用的 Vue 内置指令及其功能。
详解
| 指令 | 功能 |
|---|---|
v-once |
仅渲染一次,后续不更新 |
v-cloak |
防止页面闪动,配合 CSS 使用 |
v-bind (:) |
动态绑定属性 |
v-on (@) |
绑定事件监听器 |
v-html |
插入 HTML 字符串(注意 XSS) |
v-text |
更新文本内容 |
v-model |
表单双向绑定(语法糖) |
v-if / v-else / v-else-if |
条件渲染 |
v-show |
显示/隐藏切换 |
v-for |
列表循环渲染(需加 key) |
v-pre |
跳过编译,提升性能 |
v-memo(Vue 3.2+) |
缓存子树 |
⚠️ 安全提示:v-html 若插入不可信内容易导致 XSS 攻击,应过滤或转义。
知识点
-
v-model 本质是语法糖:对 input/checkbox/select 分别绑定 value/input 或 checked/change。
-
v-cloak 解决闪烁问题 :结合 [v-cloak] { display: none } 使用。
-
key 的重要性:v-for 中必须提供唯一 key 以提高 diff 效率。
7. 如何理解 Vue 的单项数据流?
题目重述
解释 Vue 的"单项数据流"原则及其意义。
详解
Vue 遵循 父组件 → 子组件 的单向数据流:
-
数据只能由父级通过 props 传给子组件;
-
子组件不能直接修改 props,否则会警告。
这样做的目的是防止子组件意外改变父组件状态,导致数据流混乱,难以追踪 bug。
正确做法:
-
若需本地修改,应在 data 中复制 prop 值;
-
修改后通过 $emit 通知父组件更新源数据。
反例 ❌:
TypeScript
<Child :value="msg" />
<!-- 子组件中直接 this.value = 'new' 会报错 -->
正例 ✅:
javascript
data() {
return {
localValue: this.value // 复制一份
}
},
methods: {
handleChange(val) {
this.$emit('update:value', val); // 通知父组件
}
}
知识点
-
单向数据流原则:保证数据流向清晰可控。
-
props 不可变性:禁止子组件直接修改父级传递的数据。
-
事件通知机制:通过 $emit 回传变更请求。
8. computed 和 watch 的区别及应用场景
题目重述
对比 computed 与 watch,并说明各自的使用场景。
详解
| 对比项 | computed |
watch |
|---|---|---|
| 是否缓存 | ✅ 有缓存,依赖不变不重新计算 | ❌ 每次变化都执行 |
| 是否同步 | ✅ 同步执行 | ✅ 同步,但可异步操作 |
| 适用场景 | 基于现有数据派生新值 | 响应数据变化执行副作用(如请求、定时器) |
computed 示例:
javascript
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
watch 示例:
javascript
watch: {
searchQuery(newVal) {
if (newVal.length > 2) {
this.search(); // 发起搜索请求
}
}
}
✅ 推荐:模板中展示的衍生数据用
computed;需要执行异步或复杂逻辑时用watch。
知识点
-
计算属性缓存机制:仅当依赖变化才重新求值。
-
侦听器用于副作用:适合处理异步任务或资源清理。
-
setter 可配置:computed 可设置 getter/setter 实现双向绑定。
9. v-if 与 v-for 为什么不建议一起使用?
题目重述
解释 v-if 和 v-for 不应同时使用的根本原因。
详解
Vue 的模板解析顺序是:先解析 v-for,再解析 v-if。
这意味着即使某些项不符合 v-if 条件,仍会被遍历执行,造成性能浪费。
例如:
html
<li v-for="user in users" v-if="user.active">
{{ user.name }}
</li>
→ 所有用户都会被遍历,但只渲染 active 的。
✅ 正确做法:使用 computed 预先过滤:
javascript
computed: {
activeUsers() {
return this.users.filter(u => u.active);
}
}
html
<li v-for="user in activeUsers">
{{ user.name }}
</li>
此外,v-for 应搭配唯一的 key,避免使用 index 作为 key。
知识点
-
指令优先级问题:v-for 优先于 v-if 编译。
-
性能优化手段:用计算属性提前过滤列表。
-
key 的正确用法:必须使用唯一标识(如 id),而非 index。
10. Vue 2.0 响应式数据的原理
题目重述
描述 Vue 2.x 响应式系统的实现原理。
详解
Vue 2 使用 Object.defineProperty 实现数据劫持 + 发布订阅模式。
核心流程如下:
1.数据劫持 :遍历 data 中的所有属性,使用 Object.defineProperty 为其添加 get 和 set。
javascript
Object.defineProperty(obj, 'key', {
get() { /* 依赖收集 */ },
set(newVal) { /* 派发更新 */ }
});
1.依赖收集 :在 getter 中将当前 Watcher 添加到 Dep(依赖收集器)中。
2.派发更新 :当数据变化时,在 setter 中通知所有订阅者(Watcher)更新视图。
3.观察者模式结构:
-
Observer:劫持对象属性
-
Dep:每一个属性对应一个依赖收集器
-
Watcher:具体更新行为(渲染函数、computed 等)
⚠️ 局限性:
-
无法检测对象新增属性或删除属性;
-
数组索引修改和 length 变更无法监听;
-
需通过 Vue.set 手动添加响应式属性。
知识点
-
Object.defineProperty 缺陷:无法监听动态增删属性。
-
依赖收集机制:getter 中收集 watcher,setter 中通知更新。
-
观察者模式三要素:Observer、Dep、Watcher 协同工作。
11. Vue 如何检测数组变化?
题目重述
解释 Vue 为何不能监听数组索引变化,以及如何解决。
详解
由于性能考虑,Vue 没有对数组每一项进行 defineProperty 劫持 ,而是通过 重写数组原型上的 7 个变异方法 来实现监听:
javascript
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
这些方法被拦截后,会在原有功能基础上触发视图更新。
因此以下操作不会触发更新:
javascript
this.list[0] = 'new'; // ❌ 不会触发
this.list.length = 0; // ❌ 不会触发
✅ 正确做法:
javascript
this.$set(this.list, 0, 'new'); // ✔️
this.list = this.list.splice(0, 1, 'new'); // ✔️
或者使用 Vue.set(array, index, value)。
知识点
-
数组变异方法重写:通过 AOP 思想劫持 push/pop/splice 等方法。
-
索引赋值无法监听:因 defineProperty 未对数组索引做代理。
-
Vue.set 强制响应式:用于添加新属性或更新数组元素。
12. Vue 3.0 了解多少?相比 2.0 有何改进?
题目重述
简述 Vue 3 的主要新特性及优势。
详解
主要变化:
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | Object.defineProperty | Proxy |
| API 风格 | Options API | Composition API(setup) |
| 模板语法 | slot="xxx" | <template #header> |
| v-model | 只能绑定 value | 支持多个 v-model、自定义修饰符 |
| Fragment | 不支持多根节点 | 支持 <template> 多根节点 |
| Tree-shaking | 部分支持 | 更优,按需打包 |
| TypeScript 支持 | 较弱 | 原生支持 |
核心升级点:
-
Proxy 代替 defineProperty:可监听对象新增属性、数组索引变化等。
-
Composition API:逻辑复用更灵活(类似 React Hooks)。
-
Teleport 组件:将内容渲染到 DOM 任意位置(如弹窗)。
-
Suspense:异步组件加载状态管理。
-
性能提升:更快的 diff、静态提升、block tree 优化。
setup 示例:
javascript
setup(props, { emit }) {
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
emit('add');
}
return { count, double, increment };
}
知识点
-
Proxy 优势:支持动态属性监听、数组完整监控。
-
Composition API 优势:更好的逻辑组织与复用能力。
-
Tree-shaking 支持:未使用模块不被打包,减小体积。
13. Vue 3 与 Vue 2 响应式原理区别
题目重述
对比 Vue 2 与 Vue 3 响应式实现机制的不同。
详解
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 核心 API | Object.defineProperty | Proxy |
| 监听范围 | 仅已有属性 | 新增/删除属性也可监听 |
| 数组监听 | 重写 7 个方法 | Proxy 全面拦截 |
| 性能表现 | 初始化递归劫持耗时 | 惰性代理,性能更好 |
| 兼容性 | IE9+ | IE 不支持(Proxy) |
Vue 3 使用 Proxy 包装整个对象,能拦截更多操作(共13种 trap),如 in、deleteProperty、ownKeys 等。
javascript
const reactive = (obj) => {
return new Proxy(obj, {
get(target, key) {
track(target, key); // 依赖收集
return Reflect.get(...arguments);
},
set(target, key, value) {
const res = Reflect.set(...arguments);
trigger(target, key); // 派发更新
return res;
}
});
};
知识点
-
Proxy 更强大:可监听动态属性增删。
-
Reflect 配合使用:保持默认行为一致性。
-
track/trigger 机制:Vue 3 中依赖收集的新实现方式。
14. Vue 父子组件生命周期执行顺序
题目重述
描述 Vue 加载、更新、销毁过程中父子组件生命周期的执行顺序。
详解
✅ 加载过程:
父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted
✅ 子组件更新过程:
父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
✅ 父组件更新过程:
父 beforeUpdate → 父 updated
✅ 销毁过程:
父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed
💡 提示:mounted 钩子中才能访问子组件的 DOM 或实例。
知识点
-
自上而下挂载,自下而上完成:父先准备,子先挂载,父最后完成。
-
更新顺序一致:父触发更新,子响应更新。
-
销毁逆序执行:先子后父,保障资源释放有序。
15. 虚拟 DOM 是什么?优缺点是什么?
题目重述
解释虚拟 DOM 的概念及其利弊。
详解
Virtual DOM 是用 JS 对象模拟真实 DOM 结构,是对 DOM 的抽象描述。
例如:
javascript
{
tag: 'div',
children: [
{ text: 'Hello' }
],
data: { id: 'app' }
}
Vue 在数据变化时,先生成新的 VNode,然后通过 diff 算法 对比新旧 VNode,找出最小变更集,最后批量更新真实 DOM。
优点:
-
避免频繁操作 DOM:JS 层计算变更,减少重排重绘。
-
提升开发体验:无需手动操作 DOM,专注数据逻辑。
-
跨平台能力:VNode 可渲染到 Web、Weex、SSR 等环境。
缺点:
-
首次渲染慢于 innerHTML:多了一层 VNode 构造与 diff 计算。
-
无法极致优化:框架通用性牺牲部分性能。
知识点
-
snabbdom 库借鉴:Vue 2 虚拟 DOM 基于 snabbdom 实现。
-
diff 算法核心:同层比较、key 优化、双指针对比。
-
批量更新策略:合并多次变更,减少 DOM 操作次数。
16. v-model 原理
题目重述
解释 v-model 的底层实现机制。
详解
v-model 是语法糖,根据不同的元素类型自动转换为对应的属性绑定与事件监听。
| 元素类型 | 绑定属性 | 监听事件 |
|---|---|---|
<input> / <textarea> |
value |
input |
<checkbox> / <radio> |
checked |
change |
<select> |
value |
change |
例如:
html
<input v-model="msg">
等价于:
html
<input
:value="msg"
@input="msg = $event.target.value"
>
对于组件:
html
<CustomInput v-model="msg" />
等价于:
html
<CustomInput
:modelValue="msg"
@update:modelValue="msg = $event"
/>
⚠️ 注意:中文输入法组合期间不会触发 v-model 更新,避免误输入。
知识点
-
v-model 是语法糖:简化表单双向绑定。
-
不同元素绑定不同 property:input/value vs checkbox/checked。
-
组件 v-model 默认使用 modelValue:Vue 3 中可自定义 prop 名。
17. v-for 为什么要加 key?
题目重述
解释 key 在 v-for 中的作用及重要性。
详解
key 是 VNode 的唯一标识,用于 diff 算法判断是否复用节点。
若不加 key,Vue 采用"就地复用"策略:尽可能复用相同类型的节点,可能导致状态错乱。
例如:
html
// 初始
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
// 删除第一个后
[{ id: 2, name: 'B' }]
没有 key 时,Vue 会认为第二个节点复用第一个位置,导致输入框保留原值。
加上唯一 key 后:
html
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
Vue 能准确识别节点身份,避免错误复用。
性能方面,key 可构建 map 映射,加快查找速度。
知识点
-
key 提升 diff 准确性:防止节点错位复用。
-
key 必须唯一且稳定:避免使用 index。
-
map 优化查找性能:利用 key 构建哈希映射。
18. Vue 事件绑定原理
题目重述
解释 Vue 事件绑定的内部机制。
详解
Vue 的事件分为两类:
-
原生 DOM 事件:
-
使用 addEventListener 绑定到真实元素;
-
如 @click.native。
-
-
组件自定义事件:
-
基于 on、emit 实现发布订阅模式;
-
组件间通过事件通信。
-
javascript
// 子组件
this.$emit('submit', data);
// 父组件
<Child @submit="handleSubmit" />
内部维护一个事件中心(Event Bus),存储事件名与回调映射。
.native 修饰符表示绑定原生事件,否则为组件事件。
知识点
-
发布订阅模式:on/emit 实现组件间通信。
-
原生事件与组件事件区分:native 修饰符控制绑定目标。
-
事件委托机制:Vue 在父级监听事件,提高性能。
19. vue-router 路由守卫执行顺序
题目重述
描述完整的 vue-router 导航守卫执行流程。
详解
完整导航流程如下:
-
导航被触发
-
失活组件调用 beforeRouteLeave
-
全局 beforeEach 守卫
-
重用组件调用 beforeRouteUpdate
-
路由独享守卫 beforeEnter
-
解析异步路由组件
-
激活组件调用 beforeRouteEnter
-
全局 beforeResolve(resolve)
-
导航确认
-
全局 afterEach 钩子
-
DOM 更新
-
执行 beforeRouteEnter 中的 next(vm => {...})
⚠️ 注意:beforeRouteEnter 中无法访问 this,需通过 next 回调获取组件实例。
知识点
-
路由守卫分类:全局、路由级、组件级。
-
异步组件加载:在守卫后解析。
-
next 回调传参:用于 beforeRouteEnter 中获取实例。