技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

高级前端开发者必会的 34 道 Vue 面试题系列

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/108-高级前端开发者必会的34道Vue面试题系列
本文目录35 个章节
  1. 1. MVC 和 MVVM 分别是什么
  2. 2. MVC 中 Model、View、Controller 各负责什么
  3. 3. Vue 为什么常被认为是 MVVM 的实现
  4. 4. Vue 2 的 new Vue() 初始化做了什么
  5. 5. Vue 3 的应用初始化有什么不同
  6. 6. Vue 2 响应式中的 Observer、Dep 和 Watcher 是什么
  7. 7. Vue 2 如何通过 getter/setter 收集依赖和派发更新
  8. 8. Vue 2 为什么对新增属性和数组下标有局限
  9. 9. Vue 2 为什么要改写数组方法
  10. 10. Vue 3 的 Proxy 如何处理对象和数组
  11. 11. Proxy 和 Reflect 分别解决什么问题
  12. 12. Vue 的 computed 为什么有缓存
  13. 13. watch 和 watchEffect 有什么区别
  14. 14. nextTick 是什么
  15. 15. Vue 2 的异步更新队列如何工作
  16. 16. Vue 2 的 nextTick 如何选择异步 API
  17. 17. Vue 3 的 scheduler 和 nextTick 有什么不同
  18. 18. 为什么要批量异步更新
  19. 19. Vue 能不能同步更新
  20. 20. Vue Router 解决什么问题
  21. 21. hash、history 和 memory 模式有什么区别
  22. 22. Vue Router 的导航守卫有哪些
  23. 23. 完整的导航解析过程怎样记
  24. 24. 如何理解 Vue 2 Router 的插件安装
  25. 25. Vue Router 4/5 和 Vue 2 Router 的插件机制有什么变化
  26. 26. Vue 模板为什么需要编译
  27. 27. Vue 3 编译器怎样帮助运行时优化
  28. 28. Vue 的生命周期是什么
  29. 29. beforeCreate、created、beforeMount、mounted 的区别
  30. 30. 更新阶段的 beforeUpdate 和 updated 有什么区别
  31. 31. Ajax 请求应该放在哪个钩子
  32. 32. beforeDestroy/destroyed 和 beforeUnmount/unmounted 何时使用
  33. 33. KeepAlive、errorCaptured 和 serverPrefetch 怎么理解
  34. 34. Vue 2 升级 Vue 3 时如何总结
  35. 参考资料

高级前端开发者必会的 34 道 Vue 面试题系列

原文件实际拼接了四篇旧文章:MVVM 与 Vue 响应式、Vue 2/3 数组监听、Vue 2 异步渲染与 nextTick、Vue 2 生命周期。本文保留这些文章的学习路线和源码关键词,重新整理为 34 道问题,删除抓取噪声、损坏代码和推广内容,并补充 Vue 3.5、Vue Router 4/5、Pinia、SSR 与 Nuxt 4 的现代说法。

Vue 面试题系列封面(原文图片已本地化)

1. MVC 和 MVVM 分别是什么

MVC 和 MVVM 都是组织代码职责的思想,不是某一个框架的专属实现。

MVC

  • Model:数据模型、领域数据和数据处理;
  • View:展示数据和接收用户输入的界面;
  • Controller:协调用户操作、数据请求、模型变更和视图更新。

MVVM

  • Model:业务数据;
  • View:界面;
  • ViewModel:连接 Model 和 View 的状态与绑定层。

Vue 经常被称为 MVVM 风格框架,是因为组件状态与模板之间存在响应式绑定,但 Vue 官方并不要求开发者严格按经典 MVVM 三层拆分项目。Vue 的组件、composable、store 和 renderer 共同构成了现代应用结构。

MVC 和 MVVM 的关系示意图(原文图片已本地化)

2. MVC 中 Model、View、Controller 各负责什么

以一个不依赖框架的页面为例:

<div>
  <span id="name"></span>
  <div id="data"></div>
</div>

<script>
  // Model:模拟接口或数据服务返回的数据
  function getData() {
    return {
      name: 'mvc',
      data: 'mvc 数据信息',
    }
  }

  // Controller:获取数据并决定如何更新页面
  function renderPage() {
    const result = getData()
    document.querySelector('#name').textContent = `姓名:${result.name}`
    document.querySelector('#data').textContent = result.data
  }

  renderPage()
</script>

MVC 在前后端分离后并没有消失,只是观察角度发生了变化:可以把整个前端项目当作系统的 View,也可以把接口返回的 JSON 当作 Model,把前端状态处理和页面渲染当作 Controller/View。不要把“前后端分离”理解成 MVC 彻底过时。

3. Vue 为什么常被认为是 MVVM 的实现

在理想的绑定模型中,Model 发生变化,View 自动反映变化;用户在 View 中输入,也能通过事件更新状态:

Model ↔ ViewModel ↔ View

早期 AngularJS 使用脏检查,Vue 2 使用 Object.defineProperty,Vue 3 使用 Proxy 和响应式 effect。这些都是实现绑定的不同技术。

但 Vue 并不是一个要求所有业务都遵循经典 MVVM 的完整架构。Vue 组件通常仍然采用单向数据流:父组件通过 props 传值,子组件通过 emits 通知;v-model 是这一协议的语法糖,而不是子组件直接修改父组件状态。

MVVM 中 Model、ViewModel 和 View 的关系(原文图片已本地化)

4. Vue 2 的 new Vue() 初始化做了什么

Vue 2 的简化初始化流程如下:

new Vue(options)
  → _init(options)
  → initLifecycle / initEvents / initRender
  → beforeCreate
  → initInjections / initState / initProvide
  → created
  → $mount
  → beforeMount / render watcher / mounted

initState 会处理 props、methods、data、computed 和 watch。data 对象会交给 Observer 转换为响应式数据,普通属性通过 Object.defineProperty 定义 getter/setter。

原文把“new Vue 阶段”简单写成 Observer(vm.data),真实过程还包含实例代理、依赖注入、生命周期、事件和渲染初始化,不能只用一个 Observer 概括。

Vue 2 初始化和数据绑定示意图(原文图片已本地化)

5. Vue 3 的应用初始化有什么不同

Vue 3 通过 createApp 创建应用:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.use(router)
app.use(pinia)
app.mount('#app')

每个应用实例拥有自己的应用上下文,不再依赖 Vue 2 的全局构造函数。组件在 setup()<script setup> 中声明状态、computed、watch、生命周期和依赖。

Vue 3 的响应式对象由 Proxy 代理,组件渲染通常由一个 ReactiveEffect 驱动,更新任务交给 scheduler 批处理。Vue 2 的 ObserverDepWatcher 仍有助于理解旧项目源码,但不是 Vue 3 的完整内部模型。

6. Vue 2 响应式中的 Observer、Dep 和 Watcher 是什么

Observer

递归遍历对象,为属性定义 getter/setter;对于数组则增强七个变更方法,并观察数组元素中的对象。

Dep

依赖容器。getter 读取数据时收集当前 watcher,setter 触发时通知订阅者。

Watcher

执行某种观察任务的对象,常见有:

  • render watcher:重新执行 render 并更新组件;
  • computed watcher:惰性计算并缓存 computed;
  • user watcher:执行 watch 回调。

简化的 Dep

class Dep {
  static target = null

  constructor() {
    this.subs = []
  }

  depend() {
    if (Dep.target) Dep.target.addDep(this)
  }

  notify() {
    for (const watcher of this.subs.slice()) {
      watcher.update()
    }
  }
}

依赖收集不是“页面加载时把所有 data 都订阅一次”,而是渲染 watcher 执行 render 时,读取了哪些响应式属性,就建立哪些依赖关系。

7. Vue 2 如何通过 getter/setter 收集依赖和派发更新

可以把一个响应式属性简化为:

function defineReactive(target, key, value) {
  const dep = new Dep()
  let currentValue = value

  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,

    get() {
      dep.depend()
      return currentValue
    },

    set(nextValue) {
      if (Object.is(nextValue, currentValue)) return
      currentValue = nextValue
      dep.notify()
    },
  })
}

完整 Vue 2 还会:

  1. 对嵌套对象继续 observe;
  2. 通过 child observer 的 dep 收集数组依赖;
  3. 处理新值的响应式转换;
  4. 触发 scheduler 去重和批量更新;
  5. 在组件更新后调用 updated 等钩子。

所以常见的面试回答是“getter 收集依赖,setter 派发更新”,但还应补充 watcher 和异步队列,否则答案不完整。

8. Vue 2 为什么对新增属性和数组下标有局限

Object.defineProperty 只能拦截已经定义的属性。Vue 2 中直接添加对象属性可能不会响应:

this.user.name = 'Vue' // 如果 name 初始不存在,可能无法响应
this.$set(this.user, 'name', 'Vue')

数组直接用下标赋值或修改 length 也有局限:

this.list[0] = item
this.list.length = 0

应该使用:

this.$set(this.list, 0, item)
this.list.splice(0, this.list.length)

Vue 3 中通常可以直接:

state.list[0] = item
state.list.length = 0
state.user.name = 'Vue'

但这不意味着 Vue 2 完全不能观察数组元素。已经被定义响应式的元素属性可以触发 setter;问题主要是越界新索引、length 和原生方法没有经过 Vue 2 的依赖通知。

Vue 2 基本类型响应式示例(原文图片已本地化)

Vue 2 嵌套对象响应式示例(原文图片已本地化)

9. Vue 2 为什么要改写数组方法

Vue 2 会创建一个继承自 Array.prototypearrayMethods,改写以下七个变更方法:

const arrayMethods = Object.create(Array.prototype)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

简化的 push/splice 拦截逻辑:

methodsToPatch.forEach(method => {
  const original = Array.prototype[method]

  arrayMethods[method] = function mutator(...args) {
    const result = original.apply(this, args)
    const observer = this.__ob__
    let inserted

    if (method === 'push' || method === 'unshift') {
      inserted = args
    } else if (method === 'splice') {
      inserted = args.slice(2)
    }

    if (inserted) observer.observeArray(inserted)
    observer.dep.notify()
    return result
  }
})

这不是因为 JavaScript 完全不能监听数组,而是 Vue 2 没有给所有可能的数组索引都定义 getter/setter,转而拦截有限的变更方法。数组中的对象元素仍会被递归 observe,但“替换某个索引”与“修改对象元素内部属性”是两个不同问题。

Vue 2 数组方法改写示意图(原文图片已本地化)

10. Vue 3 的 Proxy 如何处理对象和数组

Proxy 可以在对象代理层拦截 getsetdeletePropertyhasownKeys 等操作,因此 Vue 3 能够更自然地追踪:

  • 新增和删除对象属性;
  • 数组下标赋值;
  • 数组 length 变化;
  • Map/Set 的方法和迭代;
  • 嵌套对象的懒代理。

一个教学版示例:

const targetMap = new WeakMap()
let activeEffect

function track(target, key) {
  if (!activeEffect) return
  let deps = targetMap.get(target)
  if (!deps) {
    deps = new Map()
    targetMap.set(target, deps)
  }
  let dep = deps.get(key)
  if (!dep) {
    dep = new Set()
    deps.set(key, dep)
  }
  dep.add(activeEffect)
}

function trigger(target, key) {
  targetMap.get(target)?.get(key)?.forEach(effect => effect())
}

function reactive(target) {
  return new Proxy(target, {
    get(object, key, receiver) {
      const value = Reflect.get(object, key, receiver)
      track(object, key)
      return value
    },
    set(object, key, value, receiver) {
      const oldValue = object[key]
      const result = Reflect.set(object, key, value, receiver)
      if (!Object.is(oldValue, value)) trigger(object, key)
      return result
    },
  })
}

真实 Vue 3 还需要区分新增与修改、处理数组 length、避免重复代理、管理 effect scope 和 scheduler。教学代码不能替代 Vue 的响应式实现。

Vue 3 Proxy 拦截数组变化示意图(原文图片已本地化)

11. Proxy 和 Reflect 分别解决什么问题

Proxy 是代理对象操作的机制,Reflect 是与这些内部操作对应的标准方法集合。handler 中使用 Reflect,可以正确传递 receiver、继承关系和返回值:

const raw = { name: 'Vue' }
const proxy = new Proxy(raw, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  },
})

原文用 setPrototypeOf 演示返回值时有拼写和逻辑噪声。正确理解是:Reflect.setPrototypeOf(proxy, proto) 返回布尔值;如果 Proxy 的 setPrototypeOf trap 返回 falseObject.setPrototypeOf(proxy, proto) 在严格语义下可能抛出 TypeError,而 Reflect 版本直接返回 false,更适合在业务代码中检查结果。

Reflect 不是 Vue 专属,也不会自动产生响应式;只有 Vue 的 Proxy handler 在调用 Reflect 前后配合 track/trigger,才形成响应式效果。

Proxy 与 Reflect 的对象操作示例(原文图片已本地化)

12. Vue 的 computed 为什么有缓存

computed 适合表达纯派生状态:

const firstName = ref('Ada')
const lastName = ref('Lovelace')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

它的核心特点:

  1. 第一次读取时求值;
  2. 记录 getter 读取的响应式依赖;
  3. 依赖没有变化时直接返回缓存;
  4. 依赖变化后标记为 dirty;
  5. 下一次读取时重新求值。

Vue 2 通过 lazy computed watcher 和 dirty 实现,Vue 3 通过带 effect 的 computed ref 实现。computed getter 不应发请求、修改其他状态或依赖随机数;这些行为应放到 watch 或事件处理函数中。

13. watch 和 watchEffect 有什么区别

watch(source, (value, oldValue) => {
  // 明确监听 source,适合需要新旧值、取消请求和精确控制的场景。
})

watchEffect(() => {
  // 自动收集同步执行期间读取的响应式依赖。
})
  • watch 需要明确数据源,默认惰性执行;
  • watchEffect 会立即运行,并自动收集依赖;
  • 深层对象、数组、清理、flush 时机都要根据场景选择;
  • Vue 3.5 可以使用 onWatcherCleanup 清理过期请求。
import { onWatcherCleanup, watch } from 'vue'

watch(id, async currentId => {
  const controller = new AbortController()

  onWatcherCleanup(() => controller.abort())

  const response = await fetch(`/api/${currentId}`, {
    signal: controller.signal,
  })
})

onWatcherCleanup 必须在 watcher 回调的同步阶段注册,不能等异步 await 之后才注册。

14. nextTick 是什么

Vue 的响应式状态改变后,组件更新通常会被放进 scheduler 队列,而不是同步修改 DOM:

<script setup>
import { nextTick, ref } from 'vue'

const message = ref('old')
const node = ref<HTMLElement | null>(null)

async function update() {
  message.value = 'new'
  console.log(node.value?.textContent) // 可能还是 old
  await nextTick()
  console.log(node.value?.textContent) // new
}
</script>

<template>
  <p ref="node">{{ message }}</p>
  <button @click="update">更新</button>
</template>

nextTick 适合等待 Vue 当前 DOM 更新完成,不等于等待图片、网络请求或浏览器下一次绘制。如果只是 watcher 需要在 DOM 更新后执行,也可以考虑 flush: 'post'

15. Vue 2 的异步更新队列如何工作

Vue 2 中一次状态更新的典型链路是:

响应式 setter
  → dep.notify()
  → watcher.update()
  → queueWatcher(watcher)
  → nextTick(flushSchedulerQueue)
  → watcher.run()
  → vm._render()
  → vm._update()
  → patch

queueWatcher 会通过 watcher id 去重和排序。这样在同一段同步代码中:

this.value = 1
this.value = 2
this.value = 3

通常只需要把最终状态 3 渲染到页面,而不必同步提交三次中间 DOM 更新。

Vue 2 异步更新队列流程(原文图片已本地化)

16. Vue 2 的 nextTick 如何选择异步 API

Vue 2 的 nextTick 会维护 callback 队列,并根据环境选择调度方式,历史上大致是:

Promise.then
  → MutationObserver
  → setImmediate
  → setTimeout

这是 Vue 2 时代的兼容策略,不应写成所有浏览器永远固定的顺序。简化实现:

const callbacks = []
let pending = false

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice()
  callbacks.length = 0
  copies.forEach(callback => callback())
}

function nextTick(callback) {
  callbacks.push(callback)
  if (pending) return

  pending = true
  Promise.resolve().then(flushCallbacks)
}

真实源码还会处理异常、Promise 返回值和旧环境兼容。原文中代码被抓取成一行,不能直接复制运行;上面的代码只用于表达队列关系。

Vue 2 nextTick 回调队列示意图(原文图片已本地化)

17. Vue 3 的 scheduler 和 nextTick 有什么不同

Vue 3 使用 job queue、pre-flush callbacks 和 post-flush callbacks 管理更新。nextTick 的核心可以理解为:

const resolvedPromise = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

function nextTick(callback?: () => void) {
  const promise = currentFlushPromise || resolvedPromise
  return callback ? promise.then(callback) : promise
}

组件更新任务执行时,Vue 会合并同一个 job 的重复触发;nextTick 会等待当前 flush promise。Vue 3 不再沿用 Vue 2 的 Watcher 类实现,但“响应式依赖触发更新,scheduler 批量执行,最后 patch”的总体思想仍然相近。

Vue 3 scheduler 与 nextTick 示意图(原文图片已本地化)

18. 为什么要批量异步更新

批量更新有两个主要原因:

  • 性能:多次同步修改可能只需要一次最终渲染;
  • 体验:避免把中间状态短暂展示出来造成闪烁。

但“异步渲染”不是说浏览器一定等一个 setTimeout 才绘制,也不是说所有更新都延迟到宏任务。Vue 使用 scheduler 和微任务/宏任务策略协调更新,最终绘制还受浏览器渲染时机影响。

如果同步代码中有长时间的 while 循环,浏览器主线程会被阻塞,Vue 的微任务也无法执行:

state.value = 'new'
const start = Date.now()
while (Date.now() - start < 3000) {
  // 阻塞主线程,页面无法在此期间绘制
}

这不是 Vue 更新失败,而是 JavaScript 主线程被占用。耗时任务应拆分、移入 Web Worker 或采用增量计算。

批量更新合并多次状态变化(原文图片已本地化)

Vue 2 异步渲染源码调用链(原文图片已本地化)

flushSchedulerQueue 执行过程(原文图片已本地化)

watcher.run 到 patch 的更新过程(原文图片已本地化)

19. Vue 能不能同步更新

Vue 2 有 Vue.config.async = false 和内部 watcher 的 sync 路径,主要用于调试或特殊场景;它们会破坏默认批处理,不建议在业务代码中全局开启。

Vue 3 可以对单个 watcher 指定:

watch(source, callback, { flush: 'sync' })

但同步 watcher 在批量数组修改、递归状态变化和高频输入中可能造成大量执行。需要读取更新后的 DOM 时,正确做法通常是 await nextTick(),而不是强行让全部渲染同步。

20. Vue Router 解决什么问题

路由器维护 URL 与组件视图之间的映射,在 SPA 中负责:

  • 匹配路径和动态参数;
  • 更新当前 route 状态;
  • 加载异步组件;
  • 执行导航守卫;
  • 处理历史记录和滚动行为;
  • 渲染 router-view,生成 router-link

Vue Router 3 面向 Vue 2,Vue Router 4/5 面向 Vue 3。Nuxt 4 使用文件系统路由,但底层仍然依赖 Vue Router 生态能力。

21. hash、history 和 memory 模式有什么区别

hash

http://example.com/#/users

改变 hash 通常不会向服务器发送新的文档请求,部署简单,但 URL 中包含 #

history

使用 pushStatereplaceStatepopstate

const router = createRouter({
  history: createWebHistory(),
  routes,
})

URL 更自然,但服务器必须把 /users 等前端路径回退到 index.html;否则刷新深层 URL 会得到 404。

memory

在测试、SSR 或没有浏览器地址栏的环境使用内存 history,不依赖真实 URL。

22. Vue Router 的导航守卫有哪些

全局守卫

router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login' }
  }
})

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    await askForCameraPermission()
  }
})

router.afterEach((to, from, failure) => {
  if (!failure) analytics.page(to.fullPath)
})

路由独享守卫

const routes = [
  {
    path: '/admin',
    component: AdminPage,
    beforeEnter: () => {
      return isAdmin() || '/403'
    },
  },
]

组件内守卫

Vue Router 3/4/5 可以使用 Options API 的 beforeRouteLeavebeforeRouteUpdatebeforeRouteEnter;Vue 3 还可以在 setup 中使用 onBeforeRouteLeaveonBeforeRouteUpdate

现代守卫推荐返回 false、重定向位置或 true,旧版 next 回调仍可兼容,但必须确保每条分支只调用一次。

23. 完整的导航解析过程怎样记

可以按以下顺序回答:

  1. 导航被触发;
  2. 失活组件的 beforeRouteLeave
  3. 全局 beforeEach
  4. 重用组件的 beforeRouteUpdate
  5. 路由记录的 beforeEnter
  6. 解析异步路由组件;
  7. 被激活组件的 beforeRouteEnter
  8. 全局 beforeResolve
  9. 导航确认;
  10. 全局 afterEach
  11. DOM 更新;
  12. Vue Router 3 的 beforeRouteEnter 回调在实例创建后执行。

如果任意可取消守卫返回 false 或新的位置,导航会被取消或重定向;afterEach 不能取消已经确认的导航。

24. 如何理解 Vue 2 Router 的插件安装

Vue 2 使用:

Vue.use(VueRouter)

插件对象或函数会被安装一次。Vue Router 的 install 会注册全局组件、混入根实例识别逻辑,并把 $router$route 暴露给组件。

一个教学版骨架:

let Vue

class MiniRouter {
  constructor(options = {}) {
    this.mode = options.mode || 'hash'
    this.routes = options.routes || []
    this.routeMap = this.routes.reduce((map, route) => {
      map[route.path] = route.component
      return map
    }, Object.create(null))
  }
}

MiniRouter.install = function (VueConstructor) {
  Vue = VueConstructor
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this
        this._router = this.$options.router
      } else {
        this._routerRoot = this.$parent?._routerRoot
      }
    },
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot?._router
    },
  })
}

完整路由器还要实现响应式当前路由、浏览器事件、守卫、组件加载、router-linkrouter-view,不能只返回一个固定的“首页”节点。

25. Vue Router 4/5 和 Vue 2 Router 的插件机制有什么变化

Vue 3 不再通过全局 Vue 构造函数安装插件,而是安装到应用实例:

const router = createRouter({
  history: createWebHistory(),
  routes,
})

const app = createApp(App)
app.use(router)
app.mount('#app')

路由器通过 app 的 provide/inject 和全局属性向组件提供能力。实际项目不建议自行修改 $router$route,应使用 useRouter()useRoute() 和官方组件。

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
router.push({ name: 'home' })

26. Vue 模板为什么需要编译

浏览器不理解 Vue 的 {{ name }}v-ifv-for 和组件标签。构建工具会把 SFC 模板编译成 render function;使用 runtime-only 构建时,模板应该已经在构建阶段完成编译。

Vue 2 常见的源码流程是:

parse:模板字符串 → AST
optimize:标记静态节点
generate:AST → render 函数代码

原文说“模板不是正确 HTML,所以浏览器无法解析”过于简单。模板中包含 Vue 的扩展语法,浏览器可以解析其中的普通 HTML,但不能执行 Vue 指令;真正需要编译的是 Vue 模板语义。

27. Vue 3 编译器怎样帮助运行时优化

Vue 3 的 compiler-core 大致经历:

baseParse
  → transform
  → 指令/节点转换
  → generate

它可以生成:

  • 静态提升的 vnode;
  • patch flags,标记动态 text/class/style/props;
  • Block Tree,收集动态子节点;
  • 缓存事件处理器和静态属性;
  • 针对 v-ifv-for、组件和插槽的运行时代码。

因此 Vue 3 的运行时不必把模板生成的整棵树当成完全未知的动态树处理。VDOM 的性能来自响应式粒度、编译器信息和 renderer 策略的配合,而不是“虚拟 DOM 天然更快”。

28. Vue 的生命周期是什么

生命周期描述组件从创建、初始化响应式状态、挂载、更新到卸载的过程。

Vue 2 Options API:

beforeCreate → created
beforeMount → mounted
beforeUpdate → updated
beforeDestroy → destroyed

Vue 3:

setup
onBeforeMount → onMounted
onBeforeUpdate → onUpdated
onBeforeUnmount → onUnmounted

Vue 3 把 Vue 2 的 beforeDestroy/destroyed 改名为 beforeUnmount/unmountedsetup() 执行时承担了很多原来 created 阶段的初始化职责。

29. beforeCreatecreatedbeforeMountmounted 的区别

  • beforeCreate:实例初始化很早的阶段,data、methods 等还未完成;
  • created:数据、computed、watch 等已初始化,通常可以做不依赖 DOM 的请求;
  • beforeMount:组件即将进行首次挂载;
  • mounted:同步子组件已挂载,组件 DOM 已插入,可以访问 $el/ref

Vue 3 Composition API 写法:

import { onBeforeMount, onMounted } from 'vue'

onBeforeMount(() => {
  // 即将挂载
})

onMounted(() => {
  // 可以访问组件 DOM
})

onMounted 不会在 SSR 服务端执行。

beforeCreate 和 created 阶段示意图(原文图片已本地化)

created 与 DOM 尚未挂载的示意图(原文图片已本地化)

beforeMount 阶段示意图(原文图片已本地化)

mounted 阶段 DOM 已完成挂载(原文图片已本地化)

30. 更新阶段的 beforeUpdateupdated 有什么区别

beforeUpdate 在响应式状态变化、组件即将 patch 前调用,此时通常还可以读到旧 DOM;updated 在本轮 DOM 更新后调用。

import { onBeforeUpdate, onUpdated } from 'vue'

onBeforeUpdate(() => {
  // 记录更新前的 DOM 状态
})

onUpdated(() => {
  // 读取更新后的 DOM,但不要无条件改动自身依赖状态
})

对于某个点击事件后的 DOM 读取,await nextTick() 通常比把逻辑放进全局 updated 更容易理解。updated 中无条件修改状态可能形成更新循环。

同步父子组件常见的挂载顺序是:

父 beforeMount
  子 beforeCreate → created → beforeMount → mounted
父 mounted

更新时通常是父 beforeUpdate、子更新、子 updated、父 updated,但具体还会受到更新来源、scheduler 和异步组件影响。

beforeUpdate 阶段示意图(原文图片已本地化)

updated 阶段示意图(原文图片已本地化)

31. Ajax 请求应该放在哪个钩子

不能简单回答“永远放 mounted”。判断标准是请求是否依赖 DOM 和是否需要 SSR:

  • Vue 2 不依赖 DOM 的请求常放在 created
  • Vue 3 可以在 setup/composable 中初始化,也可以在 onBeforeMount 前执行;
  • 依赖浏览器 API 或 DOM 的逻辑放在 onMounted
  • Nuxt 4 需要 SSR、缓存和请求状态时使用 useFetchuseAsyncData
  • 监听路由参数重新请求时使用 watch,并清理过期请求;
  • 服务端预取使用 onServerPrefetch 或 Nuxt 数据获取能力。

请求本身通常不需要 DOM,所以把它延迟到 mounted 不一定更好;但 windowdocument、第三方图表实例等浏览器对象不能在 SSR 的 setup/created 中直接访问。

32. beforeDestroy/destroyedbeforeUnmount/unmounted 何时使用

卸载前适合清理组件创建的副作用:

  • setInterval/setTimeout
  • window/document 事件;
  • WebSocket、EventSource、BroadcastChannel;
  • IntersectionObserver、ResizeObserver;
  • 第三方图表、编辑器和播放器实例;
  • 未完成请求和订阅。

Vue 2:

beforeDestroy() {
  window.removeEventListener('resize', this.handleResize)
  clearInterval(this.timer)
  this.socket?.close()
}

Vue 3:

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
  clearInterval(timer)
  controller.abort()
})

destroyed/unmounted 执行时组件已从管理树中移除,不应再依赖其 DOM 仍然存在。Vue 2 使用 destroy 命名,Vue 3 使用 unmount 命名。

beforeDestroy 和 destroyed 的区别(原文图片已本地化)

33. KeepAlive、errorCaptured 和 serverPrefetch 怎么理解

KeepAlive

<KeepAlive> 缓存的组件切换时通常触发 activated/deactivated,而不是每次都重新 mounted/unmounted:

import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 恢复轮询或刷新可见数据
})

onDeactivated(() => {
  // 暂停轮询和昂贵监听
})

缓存组件仍然会占用内存,可以通过 includeexcludemax 控制范围。

errorCaptured / onErrorCaptured

它们用于捕获后代组件的渲染、生命周期、事件处理等错误:

import { onErrorCaptured } from 'vue'

onErrorCaptured((error, instance, info) => {
  reportError(error, info)
  return false // 阻止继续向上级 errorCaptured 传播
})

它不是完整的全局错误监控替代品,生产环境还应配置 app.config.errorHandler 和统一上报。

serverPrefetch / onServerPrefetch

Vue 2.6 的 serverPrefetch 和 Vue 3 的 onServerPrefetch 只在 SSR 预取阶段有意义,通常返回 Promise。Nuxt 应优先使用其数据获取方案,以获得缓存、payload 注水和错误处理能力。

KeepAlive、activated/deactivated 和错误处理示意图(原文图片已本地化)

serverPrefetch 和生命周期扩展示意图(原文图片已本地化)

34. Vue 2 升级 Vue 3 时如何总结

可以按能力而不是按 API 名称机械替换:

Vue 2Vue 3
new Vue()createApp()
Vue.use()app.use()
beforeDestroy/destroyedbeforeUnmount/unmounted
Object.defineProperty 响应式Proxy + effect
Vue.set/this.$set通常直接赋值,API 已移除
VueRouter 构造函数createRouter
Vuex 常见方案Pinia 常见方案
slot-scopev-slot/#
全局实例 $on/$off/$once移除,使用 props/emits 或独立 emitter

面试中可以这样作答:

Vue 是渐进式声明式 UI 框架,核心是组件化、响应式和 renderer。Vue 2 通过 Object.defineProperty、Dep、Watcher 和数组方法改写实现响应式,Vue 3 通过 Proxy、track/trigger、ReactiveEffect 和 scheduler 实现。状态变化后 Vue 会批量调度组件更新,重新生成 vnode,再由 patch 提交 DOM 操作;Vue 3 编译器还会提供静态提升、patch flags 和 Block Tree。工程上我会结合 Vue Router、Pinia、composable 和 Nuxt 组织应用,并根据场景处理 SSR、请求清理、列表虚拟化和组件边界。对于旧项目保留 Vue 2 原理有助于维护,对于新项目优先采用 Vue 3.5 的现代 API。

参考资料

原文作者:全栈者。原文件中关于 MVC/MVVM、Vue 2/3 响应式、数组监听、异步渲染、nextTick、Vue Router 和生命周期的内容予以合并保留;损坏代码、抓取标记、错误拼写、招聘推广、过时的绝对结论和 Vue 2/3 混用内容已删除或修正。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS