Skip to content

Vue 框架

1. Vue 响应式原理

题目:Vue 3 的响应式原理与 Vue 2 有什么区别?

答案要点:

特性Vue 2Vue 3
实现方式Object.definePropertyProxy
性能递归遍历对象属性懒代理,性能更好
新增属性需要 Vue.set直接赋值即可响应
数组监听重写数组方法Proxy 天然支持
兼容性IE11+现代浏览器

Vue 2 响应式:

javascript
// 递归遍历对象,为每个属性定义 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 响应式:

javascript
// 使用 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 2Vue 3 (Options API)Vue 3 (Composition API)
beforeCreatebeforeCreatesetup()
createdcreatedsetup()
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted

Composition API 示例:

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

onMounted(() => {
  console.log('组件挂载')
})

onUnmounted(() => {
  console.log('组件卸载')
})
</script>

3. Computed 和 Watch

题目:computed 和 watch 的区别是什么?

答案要点:

特性computedwatch
用途计算衍生值监听变化执行副作用
缓存有缓存无缓存
返回值
适用场景模板中的复杂计算数据变化后的异步操作

代码示例:

vue
<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内容分发默认插槽、具名插槽、作用域插槽

代码示例:

vue
<!-- 父组件 -->
<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 的导航守卫有哪些?执行顺序是怎样的?

答案要点:

导航守卫类型:

  1. 全局前置守卫 - router.beforeEach
  2. 全局解析守卫 - router.beforeResolve
  3. 全局后置钩子 - router.afterEach
  4. 路由独享守卫 - beforeEnter
  5. 组件内守卫 - beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave

执行顺序:

1. beforeEach (全局)
2. beforeEnter (路由独享)
3. beforeRouteEnter (组件内)
4. beforeResolve (全局)
5. afterEach (全局)

代码示例:

javascript
// 权限验证
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浅层响应式,减少监听

代码示例:

vue
<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 - 更好的打包优化

代码示例:

vue
<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)。

核心步骤:

  1. 同层级比较 - 只比较同一层级的节点
  2. 双端比较 - 同时从新旧节点的两端开始比较
  3. Key 优化 - 使用 key 快速定位相同节点

比较顺序:

1. 旧头 vs 新头
2. 旧尾 vs 新尾
3. 旧头 vs 新尾
4. 旧尾 vs 新头
5. 以上都不匹配,遍历查找

代码示例:

vue
<!-- 使用 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 个变异方法(pushpopshiftunshiftsplicesortreverse)来实现对数组变更的监听。

为什么 defineProperty 不能监听数组?

Object.defineProperty 的本质是拦截对象属性的 getset 操作。数组是一种特殊的对象,其索引可以看作属性,理论上也能通过 defineProperty 拦截。但出于性能考虑,Vue 2 没有这样做。如果一个数组有 1000 个元素,就需要为这 1000 个索引分别设置 getter/setter,这会导致初始化性能极差。

为什么 Proxy 可以监听数组?

Proxy 代理的是整个对象(包括数组),而不是其属性。它可以拦截对数组的各种操作,包括通过索引赋值、读取 length、以及调用任何方法。无论数组如何变化,Proxy 都能在 getset 拦截器中捕获到这些行为,从而实现真正的、全面的响应式监听。

2. 编译时优化

静态提升:

在编译阶段,Vue 3 会检测模板中的静态节点(没有绑定动态数据的元素或文本),并将它们提升到 render 函数之外。这意味着在每次重新渲染时,这些静态节点只会被创建一次,然后被复用,而不是每次都被重新创建。

补丁标记(Patch Flags):

编译器会为带有动态绑定的节点打上标记,标明哪些部分是动态的(如 :class:id、文本内容等)。在运行时更新时,Vue 可以只对比和更新这些带有标记的动态部分,而跳过对静态内容的比较,极大地提高了 diff 算法的效率。

事件缓存:

默认情况下,内联事件处理函数(如 @click="() => count++")会被缓存,避免每次渲染都生成新的函数,减少子组件的无意义更新。

3. Tree Shaking 支持

Vue 3 的核心库和功能(如 v-modeltransition、甚至响应式系统)都是通过 ES Module 的方式组织的,并且许多 API 都支持按需引入。这使得打包工具可以"摇掉"未使用的代码,大大减少了最终打包体积。

4. diff 算法优化

Vue 2 的 diff 算法:

Vue 2 的 diff 算法是全量比较的,它会遍历所有虚拟节点,并递归比较子节点数组。即使大部分内容是静态的,每次更新时也要重新比较。

Vue 3 的优化原理:

Vue 3 在编译时通过补丁标记(Patch Flags)和静态提升,为动态节点打上标记(如 TEXTCLASSPROPS 等)。在运行时更新时,diff 算法只关注带有这些标记的动态部分,而跳过对静态内容的比较。这种"靶向更新"机制极大地减少了比较的范围和深度,提升了更新性能。

Composition API vs Options API

特性Options APIComposition API
代码组织按照选项类型组织代码:datamethodscomputedwatch 等。一个功能的逻辑会被分散到不同的选项中。按照逻辑关注点组织代码:将与同一功能相关的响应式变量、方法、计算属性、侦听器等写在一起。
逻辑复用主要通过 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。

vue
<router-link :to="...">  === router.push(...)

router.replace(location)

设置 replace 属性(默认值: false)的话,当点击时,会调用 router.replace() 而不是 router.push(),于是导航后不会留下 history 记录。即使点击返回按钮也不会回到这个页面。

加上 replace: true 后,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

javascript
// 声明式:
<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 毕竟不是响应式的,取出来修改再同步,取出来用用完再同步,而且此时也不会有其他组件收到你修改的操作,如果这时候顶部导航栏菜单栏之类的也想同步个状态呢?