跳到正文
前端知识库
Vue

Vue 面试真题补充:源码、场景与工程实践

整理 Vue 面试资料中的实例初始化、响应式、组件通信、编译更新、路由权限、keep-alive、SSR 与 Vue3 迁移要点。

12 分钟Vue · Vue2 · Vue3 · 源码 · 面试

Vue 面试真题补充:源码、场景与工程实践

本文根据编号 3《Vue 面试真题》整理,保留可验证的源码链路和工程场景,并与项目已有的 Vue 基础文章互补。

1. 从 new Vue 到首屏渲染

Vue2 实例的启动可以按下面的链路回答:

  1. new Vue(options) 调用 _init
  2. 合并全局配置、组件配置和实例配置,初始化生命周期、事件和渲染能力。
  3. 执行 beforeCreate,此时 datapropsmethodscomputed 尚未完成初始化。
  4. 初始化 propsmethodsdatacomputedwatch,建立响应式关系。
  5. 执行 created,此时可以访问数据和方法,但还不能保证已经有真实 DOM。
  6. $mount 获取挂载节点;没有 render 时,将模板编译为 render 函数和静态渲染函数。
  7. mountComponent 创建渲染 Watcher,执行 _render 生成 VNode,再由 _update 调用 patch 创建或更新 DOM。
  8. 首次挂载完成后执行 mounted

因此,created 适合准备数据或发起请求,依赖 DOM 尺寸、焦点或第三方 DOM 插件的逻辑应放在 mountedthis.$nextTick 中。(知识点:生命周期与模板引用

2. Vue2 响应式的依赖链

Vue2 通过 Object.defineProperty 为对象属性定义 getter 和 setter。可以把一次更新简化为:

读取响应式属性 -> getter -> Dep 收集当前 Watcher
修改响应式属性 -> setter -> Dep.notify()
通知 Watcher -> 进入更新队列 -> 重新 render -> patch DOM

一个简化的依赖对象如下:

class Dep {
  constructor() {
    this.subscribers = new Set()
  }

  depend(watcher) {
    this.subscribers.add(watcher)
  }

  notify() {
    this.subscribers.forEach((watcher) => watcher.update())
  }
}

Vue2 的限制来自“只能拦截已经定义的属性”:

  • 直接给对象新增属性不会自动建立依赖,需要 Vue.set(target, key, value),或创建新对象替换。
  • 通过索引修改数组、直接修改 length 不能可靠触发更新,应使用 splice 或响应式数组方法。
  • 深层对象会被递归观察,数据量很大时要控制响应式范围。

Vue3 使用 Proxy 拦截对象的读取、写入、删除和属性判断,因此新增属性和数组索引的覆盖更完整;但深层对象仍然会产生代理和依赖成本,不能把所有数据无差别做成深层响应式。(知识点:响应式依赖追踪原理

3. 为什么组件的 data 必须是函数

根实例可以写对象形式的 data,组件必须返回对象的函数:

export default {
  data() {
    return { count: 0 }
  },
}

如果组件 data 直接引用同一个对象,多个实例会共享同一份状态。Vue 在初始化组件时调用 data 函数,并为每个实例得到独立对象;这既避免状态串扰,也使每个实例能独立建立响应式依赖。(知识点:组件实例与状态隔离

4. 组件通信怎么选

场景 首选方式 说明
父传子 props 单向输入,子组件不直接改写
子传父 $emit 子组件发出事件,父组件决定如何更新
访问子组件实例 ref 只用于聚焦、播放、调用公开方法等命令式场景
跨多层透传 provide/inject 适合主题、表单上下文等稳定依赖
多模块共享状态 Vuex/Pinia 需要可追踪的状态和变更流程
老项目临时解耦 EventBus 要提供取消监听,否则容易泄漏和重复响应
透传未声明属性 $attrs$listeners Vue2 常见;Vue3 中监听器已并入 $attrs

$parent$children 可以访问组件关系,但会把组件强耦合到具体层级,不应作为常规通信方案。

子组件事件示例:

<!-- Parent.vue -->
<Child @change="handleChange" />
// Child.vue
this.$emit('change', nextValue)

.sync 是 Vue2 中对“prop + update 事件”的语法糖,本质仍然是单向数据流和事件回传:(知识点:组件通信与透传边界

<Child :title.sync="title" />
this.$emit('update:title', nextTitle)

5. 模板编译、VNode 与 key

模板更新不是直接逐条执行 DOM 操作,而是经过:

template -> AST -> render function -> VNode -> patch -> DOM

key 是列表项的稳定身份,不是为了消除警告而随便添加的属性:

  • 优先使用后端或业务数据中的唯一 ID。
  • 不要使用随机数,否则每次渲染都会被视为全新节点。
  • 只有列表不会插入、删除、排序时,索引才可能作为退化方案。
  • :key="Date.now()" 会强制销毁并重建组件,适合明确需要重置内部状态的场景,不能当作常规性能优化。(知识点:模板编译与虚拟 DOM

v-ifv-for

不要把 v-ifv-for 放在同一个元素上表达筛选逻辑。筛选应放到计算属性中,或用 template v-if 包住 v-for

<template v-if="visible">
  <li v-for="item in items" :key="item.id">
    {{ item.title }}
  </li>
</template>

v-ifv-show

  • v-if 会创建和销毁子树,适合切换不频繁或初始化成本较高的内容。
  • v-show 只切换 display,适合频繁切换且需要保留组件状态的内容。(知识点:条件与列表

6. 事件与 v-model 修饰符

常见事件修饰符的语义应能直接说清:

  • .stop 调用 stopPropagation,阻止冒泡。
  • .prevent 调用 preventDefault,阻止默认行为。
  • .self 只在事件目标是当前元素时触发。
  • .capture 使用捕获阶段监听。
  • .once 只触发一次。

v-model 的常见修饰符:

  • .lazychange 而不是每次 input 更新。
  • .trim 去掉字符串两端空白。
  • .number 尝试将输入转换成数字,转换失败时保留原值。

修饰符组合应服务于明确的交互需求,不要把阻止默认行为和阻止冒泡混为一谈。(知识点:v-model

7. 更新队列与 nextTick

Vue 会把同一轮事件循环中的多次状态修改合并到异步队列,并对同一个 watcher 去重。同步读取 DOM 时,DOM 可能仍是旧值:

this.message = 'updated'
console.log(this.$el.textContent) // 仍可能是旧内容

this.$nextTick(() => {
  console.log(this.$el.textContent) // 更新后的内容
})

Vue2 优先使用原生 Promise 的微任务,其次会尝试 MutationObserver 等机制,最后才回退到定时器。nextTick 解决的是“等待本轮 DOM 更新完成”,不是让任意异步请求变同步。(知识点:调度队列与 DOM 更新时间

8. Mixins 与选项合并

Mixins 会把选项合并进组件,常见规则包括:

  • 生命周期钩子通常合并成数组,先执行 mixin,再执行组件自身钩子。
  • methodspropscomputed 等对象选项会合并,组件同名项覆盖 mixin 同名项。
  • data 会合并为独立对象,组件字段与 mixin 字段同名时以组件为准。

Mixins 的问题是来源隐式、命名容易冲突、依赖关系不明显。新代码优先使用 Vue3 composable;必须维护旧 mixin 时,应使用明确前缀并限制职责。(知识点:组合式 API 的代码复用

9. keep-alive 的缓存边界

keep-alive 是抽象组件,通过组件名称和 key 管理缓存:

<keep-alive :include="['UserList']" :max="10">
  <router-view />
</keep-alive>
  • includeexclude 可以用字符串、正则或数组匹配组件名。
  • max 控制缓存数量,超出后通常淘汰最早的缓存项。
  • 缓存组件切换时触发 activateddeactivated,不等同于 mounteddestroyed
  • 列表页的查询条件、滚动位置和请求取消策略要与缓存生命周期配套,否则可能恢复过期数据。(知识点:缓存组件与路由页面边界

10. 路由权限与 history 部署

常见权限链路是:读取 token -> 路由守卫判断白名单 -> 获取角色/权限 -> 动态注册可访问路由 -> 放行或跳转登录/无权限页。

守卫必须保证每条分支只调用一次 next(Vue Router 3)或返回一次导航结果(Vue Router 4),否则会出现重复导航或死循环。

使用 history 模式时,服务端必须把未知路径回退到 index.html

location / {
  try_files $uri $uri/ /index.html;
}

否则刷新 /detail/42 时,服务器会把它当成真实文件路径而返回 404。hash 模式不需要服务端重写,但 URL 和 SEO 能力有取舍。(知识点:缓存组件与路由页面边界

11. 错误处理与请求拦截

组件树中的渲染、生命周期和 watcher 错误可以通过 errorCaptured 向上捕获;全局兜底可配置 Vue.config.errorHandler。错误边界应记录组件名、路由、用户操作和版本信息,不能只显示一个“系统异常”。

Axios 拦截器适合统一注入 token、处理错误码和刷新凭证:

const api = axios.create({ baseURL: '/api' })

api.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${getToken()}`
  return config
})

api.interceptors.response.use(
  (response) => response.data,
  (error) => Promise.reject(error),
)

请求取消必须与组件生命周期绑定,避免离开页面后仍回写已销毁组件。新代码优先使用 AbortController;旧 Axios 版本可使用 CancelToken。(知识点:错误处理、请求取消与幂等提交

11.1 401 刷新时如何挂起并重放请求

多个请求同时收到 401 时,只允许一个刷新请求在途,其余请求等待同一个 Promise。刷新成功后,为每个原请求写入新凭证并重放;刷新失败则统一清理会话并回到登录流程。刷新接口应使用不带该响应拦截器的客户端,或显式标记为跳过刷新,否则会递归触发。

let refreshPromise = null

function refreshOnce() {
  if (!refreshPromise) {
    // requestRefreshToken 使用独立的基础客户端,避免 401 再次进入本拦截器
    refreshPromise = requestRefreshToken()
      .catch((error) => {
        clearSession()
        redirectToLogin()
        throw error
      })
      .finally(() => {
        refreshPromise = null
      })
  }
  return refreshPromise
}

api.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    const original = error.config
    const status = error.response?.status
    const isRefreshRequest = original?.url?.endsWith('/auth/refresh')

    if (status !== 401 || !original || original._retry || isRefreshRequest) {
      throw error
    }

    original._retry = true

    const accessToken = await refreshOnce()
    original.headers = {
      ...(original.headers || {}),
      Authorization: `Bearer ${accessToken}`,
    }
    return api(original)
  },
)

_retry 只防止同一个请求无限重试,不等于业务幂等。若原请求已经在服务端成功、但响应在网络中丢失,自动重放一个创建或支付请求仍可能产生重复副作用,因此这类接口还需要幂等键。(知识点:错误处理、请求取消与幂等提交

11.2 提交操作如何避免重复写入

前端禁用按钮、显示提交状态和复用在途 Promise 是交互层保护;真正的正确性必须由服务端保证。对创建订单、支付、发布等写操作,可以为一次用户意图生成一个 Idempotency-Key,重试时沿用同一个 key。服务端按“用户/租户 + key”保存处理中或已完成的结果,并配合唯一约束、事务和过期策略。

let submitting = false

async function submitOrder(payload, operationKey) {
  if (submitting) return

  submitting = true
  const key = operationKey || crypto.randomUUID()
  try {
    return await api.post('/orders', payload, {
      headers: { 'Idempotency-Key': key },
    })
  } finally {
    submitting = false
  }
}

不要在每次网络重试时重新生成 key,否则服务端无法识别它们属于同一次操作。取消请求也不能撤销已经在服务端执行的写入;错误提示和重试按钮应明确区分“未提交”“处理中”和“结果未知”。(知识点:错误处理、请求取消与幂等提交

12. qiankun 微前端的调试、资源与卸载

12.1 本地调试多个子应用

让主应用和每个子应用分别启动,主应用的注册表在开发环境指向子应用的本地入口地址。排查顺序应是:入口 HTML 是否能访问、入口脚本和样式是否返回 2xx、生命周期是否按 bootstrap -> mount -> unmount 执行、路由切换后是否仍有旧应用副作用。开发服务器需要正确配置 CORS 和 source map,端口或 public path 不一致时不要先怀疑框架。

12.2 静态资源路径

子应用的异步 chunk、CSS、图片不能依赖主应用当前 URL 进行相对解析。Webpack 子应用可以在加载其他模块前设置运行时 public path:

// public-path.js,必须在可能触发异步 import 之前执行
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

Vite 或旧的多页应用应使用对应构建工具的 base/资源前缀配置,或在网关为资源提供稳定的绝对路径。全局劫持 appendChildinnerHTML 等 DOM 方法虽然能临时改写旧资源地址,但会影响主应用和其他子应用,应只作为隔离良好、可回滚的最后手段。

12.3 沙箱不是卸载清理的替代品

代理 window 主要隔离可代理的全局属性;直接给 document.body 添加事件、修改 body class、启动定时器或注册第三方订阅,仍需要子应用自己清理。把每个副作用的撤销函数集中保存,并在 unmount 中逐个执行:

const disposers = []

export function mount() {
  const onResize = () => {}
  window.addEventListener('resize', onResize)
  disposers.push(() => window.removeEventListener('resize', onResize))
  disposers.push(() => document.body.classList.remove('child-theme'))
}

export function unmount() {
  while (disposers.length) {
    const dispose = disposers.pop()
    dispose?.()
  }
  // 同时停止轮询、Observer、WebSocket,并清理共享 store 的临时状态
}

qiankun 卸载后默认不保留子应用 DOM。需要 keep-alive 时,应把可恢复的查询条件和业务状态放到主应用或受控 store 中;若只是隐藏而不卸载,则要明确它会继续占用内存、监听器和连接。(知识点:微前端卸载与副作用清理

13. 组件库二次封装与工程闭环

面试中可以这样概括:二次封装不是复制一层 API,而是建立稳定的业务契约。透传底层组件必要的 props、attrs 和事件,同时统一主题、权限、校验、loading/error/empty 状态与可访问性;组件暴露业务语义和明确扩展点,页面不要依赖内部 DOM 结构。

工程闭环至少包括:用单元测试覆盖纯逻辑和边界,用组件/集成测试覆盖受控状态、事件、键盘和异常状态;CI 执行类型检查、构建、测试和产物检查;用语义化版本、变更记录和可回滚的不可变产物管理发布。(知识点:组件库 API 设计

14. 项目排障如何组织成可验证的回答

项目题可以按技术化的 STAR 组织:S(背景) -> T(目标) -> A(诊断与行动) -> R(结果),再补充后续动作:

  • 背景:说明用户、页面规模、约束和影响范围,不要只说“页面有问题”。
  • 目标:给出可观察的目标,例如降低白屏率、避免重复创建、缩短首屏交互时间。
  • 诊断:描述如何用 Network、Performance、日志、错误监控或最小复现定位假设。
  • 行动:说明改动、取舍和与后端/设计/测试的协作,区分临时止血与长期修复。
  • 结果:给出前后指标、异常边界和回滚方式;没有真实数据时应明确说是验证计划,不要编造数字。
  • 后续:补充监控、测试用例、文档或流程改进,证明问题不会只靠人工记忆避免。

例如“首页偶尔白屏”不能只回答“加缓存”:应先确认动态组件配置是否返回、资源请求是否失败、旧 HTML 是否引用了不存在的 chunk,再决定使用稳定资源版本、加载占位、失败降级和发布兼容窗口等措施。(知识点:PerformanceObserver 与前端监控 SDK

15. Vue3 迁移高频点

  • beforeDestroy / destroyed 对应 onBeforeUnmount / onUnmounted
  • this 不再是 setup 中组织逻辑的入口,状态和方法通过显式变量返回。
  • Vue.setVue.delete 不再需要,Proxy 会拦截新增和删除。
  • filters 被移除,使用方法或计算属性替代。
  • $listeners 合并进 $attrs,组件应显式声明 emits
  • v-model 默认使用 modelValueupdate:modelValue
  • Teleport 将弹窗、通知等内容挂载到指定 DOM 容器,解决层叠上下文和祖先 overflow 的限制。
  • createRenderer 允许把 Vue 运行时适配到非 DOM 容器,但需要实现节点创建、插入、删除和属性更新等宿主操作。(知识点:Vue3 迁移策略

面试口述模板

回答 Vue 题目时可以按“现象 -> 原理 -> 取舍 -> 场景”展开:先给出结论,再说明响应式、渲染或调度机制,接着讲边界和代价,最后结合一个具体业务场景。源码题不要只背函数名,要能指出数据从哪里进入、依赖在哪里建立、更新如何到达 DOM。