Vue 框架
1. Vue 响应式原理
题目:Vue 3 的响应式原理与 Vue 2 有什么区别?
答案要点:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 性能 | 递归遍历对象属性 | 懒代理,性能更好 |
| 新增属性 | 需要 Vue.set | 直接赋值即可响应 |
| 数组监听 | 重写数组方法 | Proxy 天然支持 |
| 兼容性 | IE11+ | 现代浏览器 |
Vue 2 响应式:
// 递归遍历对象,为每个属性定义 getter/setter
defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify();
}
}
});
}Vue 3 响应式:
// 使用 Proxy 代理整个对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}2. Vue 生命周期
题目:Vue 2 和 Vue 3 的生命周期有什么区别?
答案要点:
| Vue 2 | Vue 3 (Options API) | Vue 3 (Composition API) |
|---|---|---|
| beforeCreate | beforeCreate | setup() |
| created | created | setup() |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | beforeUnmount | onBeforeUnmount |
| destroyed | unmounted | onUnmounted |
Composition API 示例:
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件挂载')
})
onUnmounted(() => {
console.log('组件卸载')
})
</script>3. Computed 和 Watch
题目:computed 和 watch 的区别是什么?
答案要点:
| 特性 | computed | watch |
|---|---|---|
| 用途 | 计算衍生值 | 监听变化执行副作用 |
| 缓存 | 有缓存 | 无缓存 |
| 返回值 | 有 | 无 |
| 适用场景 | 模板中的复杂计算 | 数据变化后的异步操作 |
代码示例:
<script setup>
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed - 有缓存
const fullName = computed(() => {
console.log('computed 执行')
return `${firstName.value} ${lastName.value}`
})
// watch - 监听变化
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('名字变化了')
// 执行副作用,如发送请求
})
</script>4. Vue 组件通信
题目:Vue 组件之间有哪些通信方式?
答案要点:
| 方式 | 适用场景 | 示例 |
|---|---|---|
| Props / Emit | 父子组件 | defineProps, defineEmits |
| Provide / Inject | 跨层级 | provide(), inject() |
| Event Bus | 任意组件(Vue 3 不推荐) | mitt 库 |
| Vuex / Pinia | 全局状态 | createPinia() |
| Refs | 直接访问子组件 | ref + defineExpose |
| Slots | 内容分发 | 默认插槽、具名插槽、作用域插槽 |
代码示例:
<!-- 父组件 -->
<script setup>
import { provide, ref } from 'vue'
const user = ref({ name: '张三' })
provide('user', user)
</script>
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>5. Vue Router
题目:Vue Router 的导航守卫有哪些?执行顺序是怎样的?
答案要点:
导航守卫类型:
- 全局前置守卫 -
router.beforeEach - 全局解析守卫 -
router.beforeResolve - 全局后置钩子 -
router.afterEach - 路由独享守卫 -
beforeEnter - 组件内守卫 -
beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
执行顺序:
1. beforeEach (全局)
2. beforeEnter (路由独享)
3. beforeRouteEnter (组件内)
4. beforeResolve (全局)
5. afterEach (全局)代码示例:
// 权限验证
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})6. Vue 性能优化
题目:Vue 项目中有哪些性能优化手段?
答案要点:
| 优化手段 | 说明 |
|---|---|
| v-once | 只渲染一次,不更新 |
| v-memo | 条件缓存(Vue 3) |
| 虚拟列表 | 只渲染可视区域 |
| 懒加载 | 组件懒加载、图片懒加载 |
| keep-alive | 缓存组件状态 |
| 防抖节流 | 事件处理优化 |
| computed | 缓存计算结果 |
| shallowRef | 浅层响应式,减少监听 |
代码示例:
<template>
<!-- 只渲染一次 -->
<div v-once>{{ heavyContent }}</div>
<!-- 条件缓存 -->
<div v-memo="[valueA, valueB]">{{ valueA }} {{ valueB }}</div>
<!-- 组件缓存 -->
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
</template>7. Vue 3 新特性
题目:Vue 3 有哪些重要的新特性?
答案要点:
- Composition API - 更好的逻辑复用
- Teleport - 组件渲染到 DOM 其他位置
- Suspense - 异步组件加载状态
- Fragments - 组件可以有多个根节点
- Emits 选项 - 更明确的事件声明
- 全局 API 修改 -
createApp替代new Vue - Tree-shaking - 更好的打包优化
代码示例:
<template>
<!-- Teleport - 将内容渲染到 body 末尾 -->
<teleport to="body">
<div class="modal">模态框</div>
</teleport>
<!-- Suspense - 异步组件加载 -->
<suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</template>8. Vue 源码相关
题目:Vue 的 Diff 算法是如何工作的?
答案要点:
Vue 的 Diff 算法采用双端比较策略,时间复杂度为 O(n)。
核心步骤:
- 同层级比较 - 只比较同一层级的节点
- 双端比较 - 同时从新旧节点的两端开始比较
- Key 优化 - 使用 key 快速定位相同节点
比较顺序:
1. 旧头 vs 新头
2. 旧尾 vs 新尾
3. 旧头 vs 新尾
4. 旧尾 vs 新头
5. 以上都不匹配,遍历查找代码示例:
<!-- 使用 key 优化 Diff -->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>为什么不要用 index 作为 key?
- 数组中间插入元素时,index 会变化
- 导致不必要的 DOM 更新
- 可能引发状态错乱
Vue 3 高级特性
Vue 3 性能提升
Vue 3 性能提升主要通过以下几个方面体现:
1. 响应式系统(Proxy)
使用 Proxy 替代 Object.defineProperty
- 消除了 Vue 2 中对新增/删除属性、数组索引修改等无法检测的限制
- 初始化时可以只代理对象的最外层,当深层属性被访问时才进行响应式处理(惰性响应式)
- 避免了 Vue 2 中递归遍历所有属性带来的性能开销
Vue 2 监听数组的办法:
Vue 2 通过重写数组的 7 个变异方法(push、pop、shift、unshift、splice、sort、reverse)来实现对数组变更的监听。
为什么 defineProperty 不能监听数组?
Object.defineProperty 的本质是拦截对象属性的 get 和 set 操作。数组是一种特殊的对象,其索引可以看作属性,理论上也能通过 defineProperty 拦截。但出于性能考虑,Vue 2 没有这样做。如果一个数组有 1000 个元素,就需要为这 1000 个索引分别设置 getter/setter,这会导致初始化性能极差。
为什么 Proxy 可以监听数组?
Proxy 代理的是整个对象(包括数组),而不是其属性。它可以拦截对数组的各种操作,包括通过索引赋值、读取 length、以及调用任何方法。无论数组如何变化,Proxy 都能在 get 和 set 拦截器中捕获到这些行为,从而实现真正的、全面的响应式监听。
2. 编译时优化
静态提升:
在编译阶段,Vue 3 会检测模板中的静态节点(没有绑定动态数据的元素或文本),并将它们提升到 render 函数之外。这意味着在每次重新渲染时,这些静态节点只会被创建一次,然后被复用,而不是每次都被重新创建。
补丁标记(Patch Flags):
编译器会为带有动态绑定的节点打上标记,标明哪些部分是动态的(如 :class、:id、文本内容等)。在运行时更新时,Vue 可以只对比和更新这些带有标记的动态部分,而跳过对静态内容的比较,极大地提高了 diff 算法的效率。
事件缓存:
默认情况下,内联事件处理函数(如 @click="() => count++")会被缓存,避免每次渲染都生成新的函数,减少子组件的无意义更新。
3. Tree Shaking 支持
Vue 3 的核心库和功能(如 v-model、transition、甚至响应式系统)都是通过 ES Module 的方式组织的,并且许多 API 都支持按需引入。这使得打包工具可以"摇掉"未使用的代码,大大减少了最终打包体积。
4. diff 算法优化
Vue 2 的 diff 算法:
Vue 2 的 diff 算法是全量比较的,它会遍历所有虚拟节点,并递归比较子节点数组。即使大部分内容是静态的,每次更新时也要重新比较。
Vue 3 的优化原理:
Vue 3 在编译时通过补丁标记(Patch Flags)和静态提升,为动态节点打上标记(如 TEXT、CLASS、PROPS 等)。在运行时更新时,diff 算法只关注带有这些标记的动态部分,而跳过对静态内容的比较。这种"靶向更新"机制极大地减少了比较的范围和深度,提升了更新性能。
Composition API vs Options API
| 特性 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按照选项类型组织代码:data、methods、computed、watch 等。一个功能的逻辑会被分散到不同的选项中。 | 按照逻辑关注点组织代码:将与同一功能相关的响应式变量、方法、计算属性、侦听器等写在一起。 |
| 逻辑复用 | 主要通过 mixins 复用逻辑,但存在命名冲突、来源不明、隐式依赖等缺陷。 | 通过组合式函数(Composables) 复用逻辑,将相关逻辑封装在一个函数中,使用时直接调用,清晰、无冲突、类型推导友好。 |
| TypeScript 支持 | 对 TS 的支持相对有限,需要通过一些复杂的手段(如 vue-property-decorator)才能获得较好的类型推导。 | 从设计之初就为 TS 做了优化,基于函数和变量的天然类型系统,提供了极其优秀的类型推导和代码提示。 |
Vue 组件的 data 为什么是函数?
组件是可复用的 vue 实例,一个组件被创建好之后,就可能被用在各个地方,而组件不管被复用了多少次,组件中的 data 数据都应该是相互隔离,互不影响的。
基于这一理念,组件每复用一次,data 数据就应该被复制一次,之后,当某一处复用的地方组件内 data 数据被改变时,其他复用地方组件的 data 数据不受影响。
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。
Vue Router 相关
router.push 和 router.replace 的区别
router.push(location)
在 vue.js 中想要跳转到不同的 URL,需要使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,当用户点击浏览器后退按钮时,则回到之前的 URL。
<router-link :to="..."> === router.push(...)router.replace(location)
设置 replace 属性(默认值: false)的话,当点击时,会调用 router.replace() 而不是 router.push(),于是导航后不会留下 history 记录。即使点击返回按钮也不会回到这个页面。
加上 replace: true 后,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
// 声明式:
<router-link :to="..." replace></router-link>
// 编程式:
router.replace(...)
// push方法也可以传replace
this.$router.push({path: '/home', replace: true})Vuex 和 EventBus 的区别和使用场景
EventBus 适用场景
情况 1: 个人认为单页面应用的最父级组件上的提示遮罩比较适合用 EventBus 触发,因为最父级不会被销毁,EventBus 不需要重复的解绑和绑定,即使绑定很多事件的处理也不会使逻辑太乱,比如最顶层的提示弹层,加载弹层等。
情况 2: 深层级组件之间的简单通信,因为深层级组件经常被销毁重建,每次建立和移除相关 EventBus 都会有性能的开销,尤其是列表项组件,数量巨大,每个上面都写 $emit 对页面本身也不太友好,就好比 ul 的事件委托优化 li 的 onclick 事件一样,EventBus 应往父组件上绑定,通过 props 传递,但是 EventBus 交互不易过多,逻辑不直观并且不便于维护。
Vuex 适用场景
情况 1: 比如两个层级比较深的组件,左屏一个右屏一个,之间发生一些数据交互还需要同步,并且还要同步到不是最高父级上层组件中,没准还要进行一些数据记录,甚至交互其他更多的组件,props 就不用想了,一层一层上去再一层一层下来那可真蠢。
情况 2: EventBus 是一种事件的传递,a 和 b 是兄弟组件,我在 a 的子组件进行了一些修改,此时 b 还没有创建,我想渲染 b 组件并在 b 组件的子组件拿到刚才 a 组件操作的数据,然后我对 b 进行了一系列操作,想回到已被销毁的 a 组件再干点事情,EventBus 慌得一 b,此时甚至还没有 localstorage 好用,但是 localstorage 毕竟不是响应式的,取出来修改再同步,取出来用用完再同步,而且此时也不会有其他组件收到你修改的操作,如果这时候顶部导航栏菜单栏之类的也想同步个状态呢?
