面试题-Vue
一、核心概念
1. 简述 MVVM
MVVM(Model-View-ViewModel)是一种软件架构模式,将MVC中的Controller演变成ViewModel。
- Model:数据模型层,负责数据的管理和业务逻辑
- View:视图层,代表UI组件,负责数据的展示
- ViewModel:连接Model和View的桥梁,实现数据双向绑定
- 数据会自动绑定到ViewModel层并渲染到页面中
- 视图变化时会通知ViewModel层更新数据
核心特点:数据驱动视图,不再需要手动操作DOM结构。
MVVM的优点:
- 低耦合:View和Model可独立变化
- 可重用性:视图逻辑可被多个View重用
- 独立开发:开发人员和设计人员可并行工作
- 可测试性:便于进行单元测试
2. Vue底层实现原理
Vue.js采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty()
劫持各个属性的setter
和getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
核心模块:
- Observer(数据监听器):通过
Object.defineProperty()
监听数据变动,当数据变化时触发setter并通知订阅者 - Watcher(订阅者):Observer和Compile之间通信的桥梁
- 实例化时往属性订阅器(dep)添加自己
- 拥有update()方法
- 接收dep.notice()通知并触发Compile中绑定的回调
- Compile(指令解析器):解析模板指令,替换变量为数据,初始化渲染页面,并绑定更新函数
3. Vue生命周期
每个Vue实例创建时都会经过一系列初始化过程,生命周期钩子是在特定阶段触发的函数,用于执行特定操作。
主要阶段:
- create阶段:Vue实例被创建
beforeCreate
:创建前,data和methods未初始化created
:创建完毕,data和events初始化完成,可发送请求
- mount阶段:Vue实例挂载到真实DOM
beforeMount
:模板编译后,渲染前触发mounted
:渲染后触发,可操作DOM,访问$ref
- update阶段:data数据变化时触发
beforeUpdate
:数据变化后,模板更新前触发updated
:数据变化后,模板更新后触发
- destroy阶段:Vue实例被销毁
beforeDestroy
:实例销毁前,可清理事件、计时器等destroyed
:实例销毁后,可做最后的操作
组件生命周期顺序:
- 加载渲染:父beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted
- 更新阶段:父beforeUpdate → 子beforeUpdate → 子updated → 父updated
- 销毁阶段:父beforeDestroy → 子beforeDestroy → 子destroyed → 父destroyed
二、模板语法
1. computed & watch
computed(计算属性):
- 声明式描述一个值依赖其他值
- 结果会被缓存,依赖不变则不重新计算
- 必须用return返回结果
- 适用于一个属性受多个属性影响的场景(如购物车结算)
watch(属性监听):
- 监听data中已定义的变量变化
- 可执行异步或开销较大的操作
- 适用于一条数据影响多条数据的场景(如搜索功能)
建议:能用computed实现的功能优先使用computed。
2. v-for 和 v-if 为什么不建议一起使用
- Vue 2.x中,v-for优先级高于v-if,导致v-if在每个循环项上重复执行,造成性能浪费
- Vue 3.x中,v-if优先级高于v-for,语法存在歧义
- 建议:使用computed先对数据进行过滤
3. v-if 和 v-show 的区别
- v-if:编译时转化为三元表达式,条件不满足时不渲染节点
- 适用于运行时条件很少改变的场景
- v-show:编译为指令,条件不满足时通过样式隐藏节点(display:none)
- 适用于需要频繁切换条件的场景
4. v-model的实现原理
v-model
是Vue提供的语法糖,用于表单元素的双向绑定:
<input v-model="value" />
// 等价于
<input :value="value" @input="value = $event.target.value" />
5. Vue内置指令

6. Vue自定义指令
自定义指令是对HTML元素的扩展,提供自定义功能,有五个生命周期钩子:
- bind:指令第一次绑定到元素时调用,进行一次性初始化设置
- inserted:被绑定元素插入父节点时调用
- update:被绑定元素所在模板更新时调用
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用
- unbind:指令与元素解绑时调用,只执行一次
7. Vue修饰符
事件修饰符:
.stop
:阻止事件冒泡.prevent
:阻止默认行为.capture
:使用事件捕获模式.self
:仅在event.target是当前元素时触发.once
:事件只触发一次.passive
:不阻止事件的默认行为
v-model修饰符:
.lazy
:在change事件时同步数据.number
:自动将输入值转为数值类型.trim
:自动过滤输入值的首尾空格
键盘事件修饰符:
.enter
,.tab
,.delete
,.esc
,.space
,.up
,.down
,.left
,.right
系统修饰键:
.ctrl
,.alt
,.shift
,.meta
鼠标按钮修饰符:
.left
,.right
,.middle
三、组件系统
1. 组件中的data为什么是一个函数
- 组件被复用时会创建多个实例
- 若data是对象(引用类型),会导致所有实例共享同一份数据
- data为函数时,每个实例会返回独立的data对象,确保组件间数据不冲突
2. v-for中key的作用
- 帮助Vue在Diff算法执行时更快找到对应节点,提高虚拟DOM更新效率
- 避免"就地复用"策略带来的副作用
- 注意:不要使用索引作为key,可能导致状态错位;重复的key会造成渲染错误
3. Vue组件通信方式
父子组件通信:
- 父→子:props传递数据
- 子→父:$emit触发事件
- 父子实例访问:$parent / $children
跨级组件通信:
- Vuex状态管理
- $refs获取组件实例
- $attrs / $listeners
- $attrs:传递非props属性,配合interitAttrs使用
- $listeners:传递事件监听器
- Provide / inject:适用于组件库开发
兄弟组件通信:Event Bus
// 事件总线实现
class Bus {
constructor() {
this.callbacks = {};
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// 注册到Vue原型
Vue.prototype.$bus = new Bus();
// 使用方式
// 发送方
this.$bus.emit("test", data);
// 接收方
this.$bus.on("test", this.handleData);
4. 插槽
插槽是Vue实现内容分发的机制,本质是占位符。
插槽类型:
- 匿名插槽:没有名字的默认插槽
- 具名插槽:有名字的插槽,根据name属性匹配内容
- 作用域插槽:允许父组件访问子组件数据的插槽
<!-- 父组件 -->
<template #default="{ user }"><!-- Vue 2.6+语法 -->
<div>{{ user.name }}</div>
<div>{{ user.age }}</div>
</template>
<!-- 子组件 -->
<slot :user="user"></slot>
5. keep-alive
作用:Vue内置组件,用于缓存组件实例,避免组件重复创建和销毁
主要属性:
include
:需要缓存的组件名exclude
:不需要缓存的组件名max
:最多可以缓存的组件实例数量
生命周期钩子:
activated
:组件被激活时触发deactivated
:组件被停用时触发
应用场景:tabs切换、标签页、后台导航等需要保持状态的场景
使用示例:
<!-- 动态组件 -->
<keep-alive :include="whiteList" :exclude="blackList" :max="10">
<component :is="currentComponent"></component>
</keep-alive>
<!-- 路由组件 -->
<keep-alive :include="whiteList">
<router-view></router-view>
</keep-alive>
6. mixin
作用:抽离多个组件间共享的逻辑
使用场景:多个组件具有相同的逻辑(如PC端新闻列表和详情页的右侧栏目)
缺点:
- 变量来源不明确,可读性差
- 多mixin可能导致命名冲突
- mixin与组件可能形成多对多关系,增加项目复杂度
替代方案:Vue 3的Composition API
四、高级特性
1. $set方法
应用场景:解决Vue无法检测以下数据变化的问题:
- 在实例创建后添加新的属性到响应式对象
- 直接修改数组索引更新数组元素
实现原理:
- 为对象新增属性时,先将新属性转为响应式,然后触发对象
__ob__
的dep通知更新 - 修改数组索引时,内部调用数组的splice方法更新
2. nextTick
作用:在下次DOM更新循环结束后执行延迟回调,用于获取更新后的DOM
实现原理:
- 利用宏任务和微任务实现异步延迟执行
- 优先级:Promise > MutationObserver > setImmediate > setTimeout
- 多次调用会将回调存入队列,通过异步方法清空队列
3. 虚拟DOM
概念:用原生JS对象描述DOM节点的抽象层,是对真实DOM的一种轻量级表示
优点:
- 保证性能下限:提供比手动DOM操作更稳定的性能
- 无需手动操作DOM:提高开发效率
- 跨平台:便于服务器渲染、Weex等跨平台开发
缺点:
- 无法进行极致性能优化
- 首次渲染大量DOM时,可能比直接使用innerHTML慢
五、生态系统
1. Vue Router
完整的导航解析流程
- 导航被触发
- 调用失活组件的beforeRouteLeave守卫
- 调用全局的beforeEach守卫
- 调用重用组件的beforeRouteUpdate守卫(2.2+)
- 调用路由配置的beforeEnter
- 解析异步路由组件
- 调用激活组件的beforeRouteEnter
- 调用全局的beforeResolve守卫(2.5+)
- 导航被确认
- 调用全局的afterEach钩子
- 触发DOM更新
- 调用beforeRouteEnter守卫中传给next的回调
路由模式
hash模式:
- 使用URL中的#部分作为路由标识
- 兼容性好,但URL不美观
- hash变化不会触发页面重新加载,不影响后端
history模式:
- 使用HTML5 History API的pushState和replaceState方法
- URL更美观,但需要后端配置防止刷新404
- 同样不会触发页面重新加载
路由传参方式
编程式导航:
- params传参(必须搭配name使用)
this.$router.push({
name: 'test',
params: { id: 1 }
})
// 获取:this.$route.params.id
- query传参
this.$router.push({
path: '/test',
query: { id: 1 }
})
// 获取:this.$route.query.id
- 动态路由传参
// 路由配置:{ path: '/test/:id', component: Test }
this.$router.push(`/test/${id}`)
// 获取:this.$route.params.id
声明式导航:
<router-link :to="{ name: 'test', params: { id: 1 } }">测试</router-link>
<router-link :to="{ path: '/test', query: { id: 1 } }">测试</router-link>
<router-link :to="`/test/${id}`">测试</router-link>
2. Vuex
概念:专为Vue应用设计的状态管理模式,集中管理共享状态
核心特性:
- 状态存储是响应式的
- 只能通过提交mutation修改状态
- 支持模块化管理
核心模块:
- State:定义应用状态数据
- Getter:计算属性,缓存依赖结果
- Mutation:唯一修改状态的方法,必须是同步函数
- Action:提交mutation,可包含异步操作
- Module:将store拆分为多个子模块

Vuex页面刷新数据丢失问题
解决方案:Vuex数据持久化(使用本地存储保存状态)
export default function (app) {
// 页面刷新前保存状态到sessionStorage
window.addEventListener("beforeunload", () => {
sessionStorage.setItem("storeCache", JSON.stringify(app.store.state));
});
// 页面加载时恢复状态
window.addEventListener("load", () => {
const storeCache = sessionStorage.getItem("storeCache");
if (storeCache) {
app.store.replaceState(JSON.parse(storeCache));
}
sessionStorage.removeItem("storeCache");
});
}
六、性能优化
数据层优化
- 避免对象层级过深
- 非响应式数据使用Object.freeze()冻结
- 合理使用computed和watch
模板优化
- 正确区分v-if和v-show使用场景
- v-for必须添加唯一key,避免与v-if同时使用
渲染优化
- 大数据列表使用虚拟列表/虚拟表格
- 适当使用keep-alive缓存组件
资源优化
- 图片懒加载
- 路由懒加载
- 第三方插件按需引入
代码优化
- 组件销毁时清理全局变量和事件监听器
- 使用防抖和节流优化高频操作
构建优化
- 服务端渲染(SSR)或预渲染
参考资料
更新日志
38e56
-于9a964
-于8b50d
-于