前端开发面试题: Vue篇(24 题)

技术 · 2025-02-01
前端开发面试题: Vue篇(24 题)

2026-06-26T15:38:00.png

V1:Vue 有了数据响应式,为何还要 diff?

原因

  1. 响应式只能解决“是否要更新”问题:当响应式数据变化时,Vue 知道“该重新渲染了”,但不知道具体要改哪里
  2. Vue 的粒度不是单一变量:一个组件模板中可能有多个数据依赖,状态变化后需要重新执行 render 函数生成新的 VNode。
  3. diff 负责高效比较新旧 VNode 树:找出最小 DOM 操作,应用到真实 DOM。
  4. 性能优化关键:diff 使用双端对比 + 最长递增子序列算法,复杂度从 O(n³) 降到 O(n)。

总结:响应式负责调度,diff 负责精准更新


V2:Vue 3 为什么不需要时间分片?

原因

  1. 响应式粒度更细:Vue 通过 Proxy 精确知道哪个响应式数据变化,且只触发使用了该数据的组件重新渲染。
  2. 静态提升 + 补丁标记:编译期优化避免了大量 VNode 比较。
  3. 组件级调度:默认一个组件一个更新任务,粒度适合,不容易产生超长任务。
  4. 框架定位不同:Vue 设计为“渐进式”,默认不启用并发模式,代码量与心智负担更低。

React 为什么需要:不可变数据 + setState 模型下,一次更新可能涉及大量组件(任何依赖该状态的组件都重渲染),所以需要时间切片中断与优先级调度。


V3:Vue 3 为什么要引入 Composition API?

三大动机

  1. 逻辑复用:Options API 中逻辑被拆到不同选项(data / methods / computed / mounted),复用难(mixin 冲突、来源不清)。Composition API 把逻辑聚合为函数,可随意复用。
  2. 更好的 TS 支持:Composition API 主要用函数与变量,与 TS 推断结合更好。
  3. 逻辑组织灵活性:Options API 按选项分类(横向),Composition API 按逻辑关注点分类(纵向),复杂组件更清晰。

示例对比

// Options API
export default {
  data() { return { count: 0 }; },
  computed: { double() { return this.count * 2; } },
  mounted() { console.log(this.count); }
};

// Composition API
import { ref, computed, onMounted } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const double = computed(() => count.value * 2);
    onMounted(() => console.log(count.value));
    return { count, double };
  }
};

V4:谈 Vue 事件机制,手写 $on、$off、$emit、$once

Vue 2 原型事件机制:在 Vue.prototype 上维护一个事件中心。

// 简化实现
class EventBus {
  private events: Record<string, Function[]> = {};

  $on(event: string, fn: Function) {
    (this.events[event] ||= []).push(fn);
  }

  $off(event: string, fn?: Function) {
    if (!this.events[event]) return;
    if (!fn) { delete this.events[event]; return; }
    this.events[event] = this.events[event].filter(f => f !== fn);
  }

  $emit(event: string, ...args: any[]) {
    (this.events[event] || []).forEach(fn => fn(...args));
  }

  $once(event: string, fn: Function) {
    const wrapper = (...args: any[]) => {
      this.$off(event, wrapper);
      fn(...args);
    };
    this.$on(event, wrapper);
  }
}

Vue 3 移除原因:官方推荐使用外部库(mitt / tiny-emitter)或 props/emit,理由是 Vue 实例本身已足够庞大。


V5:computed 计算值为什么可以依赖另一个 computed?

const a = ref(1);
const b = computed(() => a.value * 2);
const c = computed(() => b.value + 1); // 依赖 b,b 依赖 a

// a.value = 2; → b 更新 → c 自动更新

原理

  • computed 是基于响应式依赖实现的特殊 effect;
  • 当 computed 被访问时会进行依赖收集,依赖列表中包含所有被访问的响应式源(包括其他 computed);
  • 被依赖的 computed 内部使用 dirty 标记控制是否重新计算。

优势:自动依赖追踪,调用者可当作普通 ref 使用,无需手动订阅。


V6:说一下 vm.$set 原理

Vue 2 问题:Vue 2 通过 Object.defineProperty 劫持已有属性,无法检测新增属性直接通过索引修改数组元素

$set 实现原理

function set(target: any, key: string | number, val: any) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key as number);
    target.splice(key as number, 1, val);
    return val;
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = target.__ob__;
  if (!ob) { target[key] = val; return val; }
  ob.defineReactive(target, key, val); // 关键:手动变为响应式
  ob.dep.notify(); // 手动触发更新
  return val;
}

Vue 3 已解决:基于 Proxy,无需 $set,直接赋值即可响应式。


V7:怎么在 Vue 中定义全局方法?

Vue 2

// 1. 挂到 Vue 原型
Vue.prototype.$http = axios;

// 2. 插件形式(推荐)
export default {
  install(Vue) {
    Vue.prototype.$utils = utils;
  }
};
Vue.use(MyPlugin);

Vue 3

// 1. app.config.globalProperties
app.config.globalProperties.$http = axios;

// 2. provide / inject(推荐)
app.provide('http', axios);

// 在组件中
const http = inject('http');

V8:Vue 中父组件怎么监听到子组件的生命周期?

Vue 2:通过 @hook:生命周期 事件。

<Child @hook:mounted="onChildMounted" />

Vue 3:需要在子组件显式 emit:

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['mounted']);
onMounted(() => emit('mounted'));
</script>

<!-- Parent.vue -->
<Child @mounted="onChildMounted" />

更优方案:父组件可通过 ref 获取子组件实例,调用其暴露的方法(defineExpose)。


V9:vue 组件里写的原生 addEventListener 监听事件,要手动销毁吗?

必须手动销毁

onMounted(() => {
  window.addEventListener('resize', onResize);
});
onBeforeUnmount(() => {
  window.removeEventListener('resize', onResize); // 必颁,否则内存泄露
});

原因:Vue 只会在组件卸载时清理它自己绑定的事件(通过模板 @event),不会清理通过原生 API 绑定的事件。

Vue 3 setup 自动 cleanup:使用 useEventListener 库或自实现 hook。


V10:Vue 3 中的响应式设计原理

核心:Proxy + Reflect

function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 依赖收集
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key); // 派发更新
      }
      return result;
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return result;
    }
  });
}

依赖收集

  • 每个响应式对象关联一个 dep(依赖容器);
  • 访问属性时记录当前激活的 effect;
  • 收集为 dep.subs = Set<effect>

派发更新

  • 属性变化时找到 dep,遍历 subs 依次执行 effect。

对比 Vue 2

  • Vue 2:Object.defineProperty 劫持已有属性,初始化递归遍历;
  • Vue 3:Proxy 懒加载(访问才代理)、数组下标 / length / Map / Set 全部支持。

V11:Vue 中,created 和 mounted 钩子之间的时间受什么影响?

时间差 = created 钩子执行 → render 函数生成 VNode → 首次 patch → DOM 插入 → mounted 钩子执行

影响因素:

  1. 模板复杂度:模板越复杂、节点越多,渲染耗时越长;
  2. 组件嵌套深度:嵌套越多,patch 次数越多;
  3. 同步子组件状态:子组件 init / mounted 也会占用时间;
  4. 首次渲染数据计算:复杂的 computed、watch 初始化;
  5. 业务逻辑:created 中同步耗时操作(同步请求、复杂计算)会卡在这一阶段。

优化建议

  • created 只做轻量初始化(state 准备、事件绑定);
  • 耗时逻辑放 onMounted 或异步加载。

V12:Vue 中,推荐在哪个生命周期发起请求?

推荐:createdsetup

原因

  1. 提前请求:在 DOM 渲染前发起,能提前获取数据,减少首屏加载时间;
  2. SSR 友好:服务端渲染时 created 会执行,而 mounted 不会(服务器无 DOM);
  3. 依赖收集:created 阶段访问数据也能被响应式系统追踪。

需要等待 DOM 的场景才用 mounted(如获取元素尺寸、绑定 DOM 事件)。

// 推荐写法
export default {
  async created() {
    const res = await fetchData();
    this.list = res.data;
  }
};

V13:为什么 React 需要 Fiber 架构,而 Vue 却不需要?

React 面临的问题

  • 数据不可变,状态变化触发整个组件子树重渲染
  • 需要调度器决定优先级、可以中断与恢复。

Vue 天然优势

  • 响应式粒度细:Proxy 精确追踪到哪个组件、哪个属性变化,只重新渲染实际依赖该数据的组件
  • 编译期优化:静态节点提升、补丁标记,减少 VNode 比较;
  • 架构简单:不需要时间切片、不需要并发模式也能保持良好性能。

总结:React 是“重调度 + 轻响应式”,Vue 是“重响应式 + 轻调度”。


V14:SPA 首屏加载速度慢怎么解决?

八种优化手段

  1. 包体积优化:路由懒加载、按需引入第三方库、Tree Shaking、压缩代码(terser / esbuild)。
  2. 资源优化:CDN、图片压缩(WebP/AVIF)、雪碧图、内联关键 CSS。
  3. 缓存策略:HTTP 强缓存 + 协商缓存、Service Worker、IndexedDB。
  4. 网络优化:HTTP/2 多路复用、域名分片、预解析 DNS、预连接(preconnect)、预加载(preload)。
  5. SSR / SSG:Nuxt / Next.js、服务端预渲染、静态化。
  6. 骨架屏 / Loading:优化感知体验。
  7. 首屏数据预取:路由进入前预取数据。
  8. 监控 & 调优:Lighthouse、WebPageTest、PerformanceObserver。

V15:说下 Vite 的原理

核心:浏览器原生 ESM + esbuild 预构建

冷启动

  1. 预构建依赖(基于 esbuild):用 esbuildnode_modules 中的 CommonJS / UMD 模块转换为 ESM(快 10-100 倍)。
  2. 按需编译:浏览器请求哪个模块,Vite 才用 esbuild 转换该模块(转换与请求并行),返回后浏览器继续请求依赖模块。
  3. 缓存:强缓存(HTTP 头)+ 协商缓存(304)。

热更新 HMR

  1. 通过 WebSocket 监听文件变化;
  2. 变动的模块被 esbuild 重新编译;
  3. 框架插件接收更新(vue-loader / react-refresh),热替换运行中的模块。

生产构建:使用 Rollup(生态成熟、产物优化好)。

为什么快

  • 不走打包:无需全量构建;
  • 按需编译:用哪个编哪个;
  • 原生 ESM:浏览器原生支持,无需 polyfill。

V16:Vue 2 为什么不能检测数组变化?怎么解决?

原因:Vue 2 通过 Object.defineProperty 劫持,数组的以下操作不会被检测

  • 直接通过索引赋值:arr[0] = 1
  • 修改 length:arr.length = 0
  • 部分方法(Vue 2 已重写了 7 个变更方法:push/pop/shift/unshift/splice/sort/reverse)。

解决方案

// 1. Vue.set / this.$set
this.$set(this.arr, index, value);

// 2. 替换数组(推荐)
this.arr.splice(index, 1, value);

// 3. 整体重新赋值
this.arr = [...this.arr.slice(0, index), value, ...this.arr.slice(index + 1)];

Vue 3 已根治:基于 Proxy,所有数组操作天然可追踪。


V17:说说 Vue 页面渲染流程

Vue 3 渲染流程

  1. 编译阶段:模板 → AST → 渲染函数(带静态提升、补丁标记优化)。
  2. 挂载阶段(首次渲染):

    • 创建组件实例(setup);
    • 执行 render 函数生成 VNode 树;
    • 调用 patch 函数;
    • 首次 patch走挂载逻辑:创建真实 DOM、插入到容器、绑定事件;
    • 触发 onMounted 钩子。
  3. 更新阶段

    • 响应式数据变化触发 effect;
    • 调度器(scheduler)将更新加入微任务队列(nextTick);
    • 重新执行 render 函数生成新 VNode;
    • 调用 patch 函数进行 diff 比较
    • 将差异(DOM 操作)应用到真实 DOM;
    • 触发 onUpdated 钩子。
  4. 卸载阶段:触发 onBeforeUnmount / onUnmounted,移除 DOM、解绑事件、清理 effect。

V18:Vue 中 computed 和 watch 区别

维度computedwatch
用途派生新值监听数据变化执行副作用
缓存有缓存,依赖不变不重算无缓存
返回必须 return不需要
异步不支持异步支持异步
调用时机访问时惰性求值数据变化立即/深度监听
适用模板渲染、数据转换路由变化、数据持久化、接口请求
// computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// watch
watch(searchKey, async (newVal) => {
  const res = await api.search(newVal);
  list.value = res.data;
}, { debounce: 300, immediate: true });

V19:Vuex 中的辅助函数怎么使用?

辅助函数mapStatemapGettersmapMutationsmapActionscreateNamespacedHelpers

// Options API 中
import { mapState, mapMutations } from 'vuex';

export default {
  computed: {
    ...mapState(['count']),
    ...mapState('user', ['name'])
  },
  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['fetchData'])
  }
};

// Composition API 中(推荐)
import { useStore } from 'vuex';
import { computed } from 'vue';

export default {
  setup() {
    const store = useStore();
    return {
      count: computed(() => store.state.count),
      increment: () => store.commit('increment')
    };
  }
};

Vue 3 推荐 Pinia:更轻量、Composition API 友好、TypeScript 友好。


V20:用 Vue 3 实现一个 Modal,怎么设计?

设计要点

  1. Teleport 渲染到 body:避免被父元素裁剪 / 滚动;
  2. 受控 / 非受控v-model 双向绑定显隐;
  3. 事件open / close / confirm / cancel
  4. 插槽:title、default、footer;
  5. 可配置:尺寸、遮罩、点击遮罩关闭、ESC 关闭、滚动锁定。
<template>
  <Teleport to="body">
    <Transition name="modal">
      <div v-if="modelValue" class="modal-mask" @click.self="close">
        <div class="modal" :style="{ width: width }">
          <header v-if="$slots.title">
            <slot name="title" />
            <button @click="close">×</button>
          </header>
          <main><slot /></main>
          <footer v-if="$slots.footer">
            <slot name="footer" :confirm="confirm" :cancel="cancel" />
          </footer>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script setup lang="ts">
const props = defineProps<{ modelValue: boolean; width?: string }>();
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);

const close = () => emit('update:modelValue', false);
const confirm = () => emit('confirm');
const cancel = () => emit('cancel');

const onKey = (e: KeyboardEvent) => e.key === 'Escape' && close();
watch(() => props.modelValue, (v) => {
  document.body.style.overflow = v ? 'hidden' : '';
  if (v) document.addEventListener('keydown', onKey);
  else document.removeEventListener('keydown', onKey);
});
</script>

V21:Vue 3 Tree Shaking 特性是什么?举例说明

原理:ES Module 静态结构使得打包工具可以分析哪些导出未被使用,删除未引用的代码。

Vue 3 的优势

  • 全部 API 采用 ESM 导出,按需引入;
  • 编译器、运行时、服务端渲染分离为独立包;
  • v-model / 自定义指令等按功能分包。
// 只引入需要的 API(Tree-shakable)
import { ref, computed, onMounted } from 'vue';

// 未被引入的 API(如 v-show / Transition)不会出现在最终 bundle 中

对比 Vue 2:Vue 2 将所有 API 挂载到 Vue 单例上(Vue.nextTickVue.set),打包工具无法静态分析,默认全部打包进去。


V22:Vue 3 Composition API vs Vue 2 Options API

维度Options APIComposition API
组织方式按选项分类(data/methods/...)按逻辑关注点聚合
代码复用mixin(覆盖/来源不清晰)自定义 Hook 函数
TS 推断一般优秀
学习曲线低,上手快中,需要理解响应式原理
适用场景小型组件、简单逻辑复杂组件、大型企业应用
响应式原理透明性隐藏需手动管理

推荐策略:复杂组件用 Composition API,简单展示组件用 Options API(Vue 3 同时支持)。


V23:Vue 3 性能提升主要体现在哪几方面?

  1. 响应式系统升级:Proxy 替代 defineProperty,支持数组下标 / Map / Set,新增属性自动响应式。
  2. 编译期优化

    • 静态节点提升(PatchFlag);
    • 事件监听缓存;
    • 树摇友好(按需引入)。
  3. VNode 优化:单个 VNode 体积减少,patch 更快。
  4. SSR 优化@vue/server-renderer 流式输出。
  5. 体积更小:核心 ~34KB(Vue 2 ~50KB),Tree Shaking 进一步减少实际包体积。
  6. Composition API:逻辑复用更高效,减少 mixin 带来的不必要渲染。

V24:Vue 3 的设计目标是什么?做了哪些优化?

设计目标

  1. 更好的 TS 支持:大型项目类型安全;
  2. 更好的逻辑复用:解决 mixin 缺陷;
  3. 更好的性能:编译期 + 运行时全面优化;
  4. 更小的体积:Tree-shakable;
  5. 更灵活的 API:同时支持 Options 和 Composition;
  6. 为未来铺路:为 Vapor 模式(无 VNode)和自定义渲染器打基础。

主要优化

维度优化手段
响应式Proxy 替代 defineProperty
编译PatchFlag、静态提升、缓存事件
Diff最长递增子序列算法减少 DOM 移动
SSR流式渲染、组件级缓存
包体积Tree Shaking、按需打包

本文作者:小码哥

本文链接:https://wesee.club/archives/1181/

版权声明:自由转载-非商用-非衍生-保持署名(cc 创意共享 3.0 许可证

Theme Jasmine by Kent Liao

粤ICP备2023052298号-1