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

显示模式

登录
ARCHIVE DOCUMENTVUE

2.6W 字 Vue 经典面试题源码级详解(整理与更新版)

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/71-历时一个月,2 6W字!50+Vue经典面试题源码级详解,你值得收藏!
本文目录25 个章节
  1. 01 Vue 组件之间有哪些通信方式
  2. 02 v-if 和 v-for 哪个优先级更高
  3. 03 Vue 生命周期以及每个阶段做什么
  4. 04 Vue 的双向绑定使用和原理
  5. 05 如何扩展一个 Vue 组件
  6. 06 子组件可以直接改变父组件的数据吗
  7. 07 Vue 权限管理如何做到按钮级别
  8. 08 如何理解 Vue 响应式
  9. 09 如何理解虚拟 DOM
  10. 10 了解 Diff 算法吗
  11. 11 Vue 3/3.5 有哪些新特性
  12. 12 如何定义动态路由并获取参数
  13. 13 从零实现一个简易 Vue 路由
  14. 14 key 的作用
  15. 15 nextTick 的使用和原理
  16. 16 watch 和 computed 的区别与选择
  17. 17 父子组件创建和挂载顺序
  18. 18 如何缓存组件,缓存后如何更新
  19. 19 从零构建一个 Vue 项目
  20. 20 实际工作中的 Vue 最佳实践
  21. 21 如何理解 Vuex
  22. 22 从 template 到 render 的处理过程
  23. 23 Vue 实例挂载过程中发生了什么
  24. 总结
  25. 参考资料

2.6W 字 Vue 经典面试题源码级详解(整理与更新版)

原文是一篇覆盖面很广的 Vue 面试题合集。本文保留原来的 23 个主题、回答思路和源码分析方向,删除课程推广、抓取噪声和失效链接,并补充 Vue 2.7、Vue 3.5、Vue Router 4/5、Pinia 及 Nuxt 4 语境。源码片段均为教学化简化代码,不应当当作可直接替换的框架源码。

01 Vue 组件之间有哪些通信方式

可以先按组件关系回答,再补充特殊场景:

关系常用方式
父子props、emits、v-model、模板 ref、$attrs
跨层级provide/inject、状态管理、组合式函数
兄弟或跨模块共同父状态、Pinia、明确的事件/消息通道
DOM 或组件实例能力模板 ref,尽量只暴露必要方法

Vue 组件通信关系示意图(已本地化)

1. props / emits

父组件通过 props 下传,子组件通过事件通知父组件:

<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const message = ref('Hello')
</script>

<template>
  <Child
    :message="message"
    @update:message="message = $event"
  />
</template>
<!-- Child.vue -->
<script setup lang="ts">
const props = defineProps<{ message: string }>()
const emit = defineEmits<{
  'update:message': [value: string]
}>()

function update() {
  emit('update:message', `${props.message}!`)
}
</script>

<template>
  <button @click="update">{{ message }}</button>
</template>

实际使用时可以直接写成:

<Child v-model:message="message" />

2. provide/inject

适合主题、表单上下文、依赖服务等跨层级依赖,不适合把所有业务状态都藏进注入对象:

// provider
provide('formContext', { validate })

// descendant
const form = inject<{ validate: () => Promise<boolean> }>('formContext')

3. 需要避免的旧 API

Vue 2 常见的 $parent$children$root 和 event bus 可以在旧项目中维护,但它们会产生隐式依赖。Vue 3 已移除 $on$off$once$children$listeners$attrs 仍然存在,但 Vue 3 已把未声明的监听器纳入 $attrs

复杂应用中的共享状态优先使用 Pinia。Nuxt 4 应遵循其状态管理约定,并注意 SSR 中不能让请求之间共享可变用户状态。

02 v-ifv-for 哪个优先级更高

实践结论:不要把 v-ifv-for 写在同一个元素上。

版本差异:

  • Vue 2:同一元素上 v-for 优先级高于 v-if,会先遍历再判断;
  • Vue 3:v-if 优先级高于 v-for,所以 v-if 表达式不能直接访问该元素中由 v-for 声明的变量。

Vue 3 同时使用 v-if 与 v-for 的错误示意图(已本地化)

过滤列表:使用 computed

<script setup lang="ts">
import { computed, ref } from 'vue'

const users = ref([
  { id: 1, name: 'Ada', active: true },
  { id: 2, name: 'Grace', active: false },
])

const activeUsers = computed(() => {
  return users.value.filter(user => user.active)
})
</script>

<template>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</template>

控制整组列表:移动到容器

<ul v-if="shouldShowUsers">
  <li v-for="user in users" :key="user.id">
    {{ user.name }}
  </li>
</ul>

如果不方便添加真实容器,也可以使用 <template v-if> 包住列表。即使两个指令在某个版本能够运行,也不代表这是可读、可维护或高效的写法。

03 Vue 生命周期以及每个阶段做什么

生命周期钩子是组件在创建、挂载、更新和卸载等阶段提供的扩展点。

Vue 2.7Vue 3.5说明
beforeCreatebeforeCreate实例选项初始化早期
createdcreatedOptions API 状态和事件已初始化
beforeMountonBeforeMount首次 DOM 挂载前
mountedonMounted组件 DOM 已插入
beforeUpdateonBeforeUpdate响应式更新提交前
updatedonUpdatedDOM 更新后
beforeDestroyonBeforeUnmount卸载前
destroyedonUnmounted卸载完成
activatedonActivatedKeepAlive 缓存组件激活
deactivatedonDeactivatedKeepAlive 缓存组件停用
errorCapturedonErrorCaptured捕获后代组件错误
serverPrefetchonServerPrefetchSSR 服务端预取,需对应 SSR 环境

Vue 3 还提供 onRenderTrackedonRenderTriggered 调试钩子。它们不是普通业务生命周期,不应在生产逻辑中依赖。

Vue 生命周期流程图(已本地化)

常见使用场景

  • createdsetup:准备状态、创建非 DOM 依赖;
  • mounted:访问浏览器 DOM、初始化第三方 DOM 库;
  • updated:确实需要在组件更新后操作 DOM 时使用,避免在其中再次无条件修改状态;
  • beforeUnmount/onBeforeUnmount:取消订阅、移除事件、停止定时器;
  • unmounted/onUnmounted:释放最终资源;
  • onActivated/onDeactivated:恢复或暂停缓存页面的轮询。

setup() 在 Vue 3 创建阶段执行,通常早于 Options API 的 created,但它不是一个可以简单替换所有 created 逻辑的同名钩子。SSR 不会执行 onMounted,因为服务端不存在浏览器 DOM。

04 Vue 的双向绑定使用和原理

v-model 可以理解为:

Model → View:属性/响应式渲染
View → Model:input/change 等事件

原生表单元素

<script setup lang="ts">
import { ref } from 'vue'

const message = ref('')
const checked = ref(false)
const selected = ref('vue')
</script>

<template>
  <input v-model="message">
  <input v-model="checked" type="checkbox">
  <select v-model="selected">
    <option value="vue">Vue</option>
    <option value="react">React</option>
  </select>
</template>

Vue 会根据元素类型使用不同的 property 和事件:文本输入通常对应 value/input,checkbox 对应 checked/change,select 对应 value/change.lazy.number.trim 可以改变同步时机或值处理,但 .number 不是严格类型验证。

自定义组件

Vue 3 默认展开为:

<CustomInput
  :model-value="message"
  @update:model-value="message = $event"
/>

组件内部:

<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

function onInput(event: Event) {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

<template>
  <input
    :value="props.modelValue"
    @input="onInput"
  >
</template>

Vue 3.4+ 可以使用 defineModel() 简化组件写法;Vue 2 默认是 value prop 和 input 事件,也可以通过组件 model 选项修改名称。Vue 3 支持 v-model:title 等多个绑定参数,.sync 不再是推荐的组件通信方式。

05 如何扩展一个 Vue 组件

可以按“逻辑复用”和“内容复用”回答:

  • Composition API/composable:Vue 3 首选的逻辑复用方式;
  • mixins:Vue 2 历史项目常见,但来源不透明、容易命名冲突;
  • extends:Options API 级别的配置合并,使用较少;
  • slots:内容分发,不等同于逻辑复用;
  • render function / higher-order component:特殊组件库场景使用。

composable

// useCounter.ts
import { computed, ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  const increment = () => count.value++

  return { count, double, increment }
}
<script setup lang="ts">
import { useCounter } from './useCounter'

const { count, double, increment } = useCounter()
</script>

slots

<!-- Child.vue -->
<template>
  <section class="card">
    <header><slot name="title">默认标题</slot></header>
    <main><slot /></main>
  </section>
</template>
<Child>
  <template #title>用户列表</template>
  <UserList />
</Child>

Vue.extend 是 Vue 2 的构造器 API,Vue 3 已移除;不能把 Vue 2 的 Vue.extend 代码直接迁移到 Vue 3。Vue 3 应使用 defineComponenth、composable 或普通组件组合。

06 子组件可以直接改变父组件的数据吗

原则上不应该。props 是父向子的单向下行绑定,子组件不应直接改写 prop:

<script setup lang="ts">
const props = defineProps<{ value: string }>()

// ❌ props.value = 'new value':只读 prop,Vue 会警告
</script>

正确做法是 emit 事件,由父组件修改状态:

<script setup lang="ts">
const props = defineProps<{ value: string }>()
const emit = defineEmits<{ change: [value: string] }>()

function changeValue(value: string) {
  emit('change', value)
}
</script>

如果 prop 只是初始值,可以复制到本地 ref;如果只是格式化,用 computed:

const props = defineProps<{ initialCount: number, size: string }>()
const count = ref(props.initialCount)
const normalizedSize = computed(() => props.size.trim().toLowerCase())

对象和数组 prop 的嵌套属性在 JavaScript 上仍可能被子组件修改,因为引用是共享的;Vue 不能完全阻止这种行为,但应把它视为隐式耦合,优先通过 emit、store action 或更新整个对象来保持数据流清晰。

07 Vue 权限管理如何做到按钮级别

权限通常分成:

  1. 页面/路由权限:用户能否进入某个路由;
  2. 操作权限:页面中能否看到或使用某个按钮;
  3. 服务端资源权限:API 最终是否允许操作。

前端权限控制只能改善导航和展示,不能作为安全边界。用户可以绕过隐藏按钮直接请求 API,后端必须再次验证角色、权限和资源归属。

路由权限

Vue Router 4/5 可以在路由 meta 中声明权限:

const routes = [
  {
    path: '/users',
    component: () => import('./views/UsersView.vue'),
    meta: { permissions: ['user:read'] },
  },
]

守卫推荐返回 truefalse 或重定向位置:

router.beforeEach(to => {
  if (!to.meta.permissions?.length) return true
  if (auth.hasAll(to.meta.permissions)) return true
  return { name: 'forbidden' }
})

Vue Router 3 的旧项目仍常见 next 回调,但要保证每条路径只调用一次。

服务端返回路由时,前端不能直接把不可信的字符串当作任意组件 import。应使用组件白名单映射:

const componentMap = {
  Users: () => import('./views/UsersView.vue'),
  Reports: () => import('./views/ReportsView.vue'),
}

Router 3 使用 router.addRoutes(),Router 4/5 使用 router.addRoute();后者一次添加一个路由记录或嵌套路由。动态添加后如果当前地址需要重新匹配,可以调用 router.replace(router.currentRoute.value.fullPath)

按钮权限

可以用 v-permission 控制展示,但需要监听权限异步更新,不要只在首次 mounted 时永久删除节点。权限指令的完整实现可参考 69-分享8个非常实用的Vue自定义指令

前端与服务端权限数据流示意图(已本地化)

08 如何理解 Vue 响应式

响应式是:状态被读取时记录依赖,状态变化时通知依赖并执行相应更新。

Vue 2.7

  • 普通对象:Object.defineProperty 的 getter/setter;
  • 依赖:Dep 收集 Watcher
  • 数组:拦截部分变更方法;
  • 新增/删除属性:需要 Vue.set/this.$setVue.delete 等兼容 API;
  • Map/Set:不具备 Vue 3 那样完整的原生集合响应式支持。

Vue 3.5

  • reactive 对象:Proxy/Reflect;
  • 基础值:ref
  • 派生值:computed
  • 副作用:watch/watchEffect
  • 集合:对 Map/Set 等操作提供专门的依赖处理;
  • 调度:scheduler 批量安排组件和 watcher 更新。

Proxy 改善了新增属性、删除属性、数组索引和集合等操作的覆盖范围,但它不能完整 polyfill 到 IE11,也不是没有运行时成本。大对象、不可变数据和外部库实例有时适合 shallowRefmarkRaw

09 如何理解虚拟 DOM

VNode 是描述 UI 的 JavaScript 对象。模板经 compiler 编译为 render function,render function 返回 VNode,renderer 再将 VNode 挂载或 patch 到宿主环境:

模板、render、VNode 和 DOM 的流程示意图(已本地化)

Virtual DOM 的价值包括:

  • 声明式描述 UI,减少手动维护 DOM;
  • 统一处理节点复用、属性更新、插入、移动和删除;
  • 配合 scheduler 批量更新;
  • 通过不同 renderer 支持 SSR 或自定义平台。

但不能说 Virtual DOM 天生更快,也不能说它保证最小 DOM 操作。VNode 创建、Diff 和调度本身有成本;Vue 3 的性能还来自静态提升、Patch Flags、Block Tree 和 compiler-informed rendering。详情可参阅 62-你对虚拟DOM原理的理解

10 了解 Diff 算法吗

Diff/patch 的作用是比较新旧渲染结果,并把必要的变化提交给 renderer。常见假设是:

  1. 主要做同层比较,不做任意跨层树编辑;
  2. 不同节点类型通常替换子树;
  3. key 用来表达列表节点身份;
  4. 编译器尽量跳过静态内容。

Vue 2.7

Vue 2 的 updateChildren 使用双端指针,尝试比较:

  • 旧头/新头;
  • 旧尾/新尾;
  • 旧头/新尾;
  • 旧尾/新头;
  • 都不匹配时通过 key map 查找、移动或创建节点。

Diff 前后节点树示意图(已本地化)

Vue 3.5

Vue 3 的 keyed children 更新会先处理相同前缀和后缀,再建立 key 到新索引的映射,处理新增、删除和移动;需要移动时会计算最长递增子序列(LIS),尽量保留已经按新顺序排列的节点。

“Diff 算法是 O(n),所以一定快”是不完整的。算法复杂度只是模型,真实性能还受 VNode 数量、组件更新范围、属性 patch、布局/绘制、事件和业务计算影响。

11 Vue 3/3.5 有哪些新特性

API 与组件能力

  • Composition API:refreactivecomputedwatch
  • <script setup>
  • Fragment:组件可以有多个根节点;
  • Teleport
  • Suspense:异步依赖和 fallback,部分高级能力仍需结合版本/框架验证;
  • defineAsyncComponent
  • emits 声明;
  • defineModel(Vue 3.4+);
  • useTemplateRefonWatcherCleanup 等后续 API;
  • 自定义 renderer;
  • CSS v-bind()

Vue 3 特性列表示意图(已本地化)

框架层面的改进

  • Proxy 响应式和独立 reactivity 包;
  • compiler 的静态提升、Patch Flags、Block Tree;
  • 更好的 tree-shaking;
  • TypeScript 重写和模块化;
  • Composition API 更适合按逻辑组织大型组件。

“Vue 3 更快、更小”是相对于对应版本和构建方式的总体方向,不应理解为任何页面都自动更快。Suspense、SSR、hydration 和 Nuxt 的行为还要看具体框架版本。

12 如何定义动态路由并获取参数

Vue Router 4/5:

const routes = [
  {
    path: '/users/:id',
    component: () => import('./views/UserView.vue'),
  },
  {
    path: '/posts/:username/:postId',
    component: () => import('./views/PostView.vue'),
  },
]

在 Composition API 中:

<script setup lang="ts">
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

watch(
  () => route.params.id,
  id => loadUser(String(id)),
  { immediate: true },
)
</script>

Options API 使用 this.$route.params.id。查询参数和 hash 分别在 route.queryroute.hash 中。路由组件在同一路径记录下切换参数时通常会复用实例,不会重新执行完整 mounted,因此要 watch 参数或使用路由守卫。

捕获 404 可以把通配路由放在合适位置:

{ path: '/:pathMatch(.*)*', component: NotFoundView }

Vue Router 3 的写法是 path: '*',不能把两种版本语法混用。

13 从零实现一个简易 Vue 路由

路由器需要解决:URL 改变但页面不整页刷新,并根据当前地址渲染匹配组件。

基本设计

  1. 保存 routes 配置;
  2. 根据 hash 或 history 读取当前 location;
  3. 监听 hashchangepopstate
  4. 匹配路由记录;
  5. 提供 RouterLinkRouterView
  6. 处理 base、404、嵌套路由和导航守卫。

现代 Vue Router 使用:

import {
  createRouter,
  createWebHistory,
} from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

hash 模式使用 createWebHashHistory(),不需要服务器配置 fallback;history 模式需要服务器把未知前端路径回退到 index.html,否则刷新深层 URL 可能返回 404。

RouterLink 负责导航,RouterView 根据当前匹配记录渲染组件。真正的路由还要处理编码、动态参数、query、嵌套、滚动行为、异步组件和导航守卫,所以手写 demo 只适合理解原理。

Vue Router 4/5 的守卫推荐返回值:

router.beforeEach(to => {
  if (to.meta.requiresAuth && !auth.isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
  return true
})

14 key 的作用

key 用于表达 VNode/列表项目的身份,帮助 renderer 判断旧节点和新节点是否代表同一个实体:

<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

列表发生插入、删除、排序时,稳定 key 可以让 Vue 找到对应旧节点,尽量复用并移动 DOM;没有 key 时,Vue 可以采用就地 patch,按位置复用节点。无 key 不等于“永远认为任意两个节点相同”,节点类型和上下文仍然参与判断。

不要把数组 index 当成所有列表的 key。静态、只追加且不重排的列表可能没有明显问题,但可插入、删除、排序或含输入状态的列表应使用业务 ID。

key 也可用于强制重建组件:

<Editor :key="documentId" :document="document" />

同一标签过渡切换不同内容时设置不同 key,可以让 Transition 识别为不同节点。key 必须是稳定的字符串或数字,不能使用每次 render 都变化的随机值。

没有 key 时的节点复用示意图(已本地化)

使用 key 后的节点匹配示意图(已本地化)

keyed diff 的移动示意图(已本地化)

15 nextTick 的使用和原理

Vue 会把响应式更新放入调度队列并批量刷新。修改状态后,如果同一段代码要读取更新后的 DOM,可以使用:

import { nextTick, ref } from 'vue'

const count = ref(0)
const element = ref<HTMLElement | null>(null)

async function increment() {
  count.value++
  await nextTick()
  console.log(element.value?.textContent)
}

Vue 2 使用 this.$nextTick(),Vue 3 可使用 await nextTick()。它等待的是 Vue 当前的 DOM flush,不等于 setTimeout(0)、Node 的 process.nextTick,也不保证浏览器已经完成绘制。需要下一次渲染机会时再结合 requestAnimationFrame

Vue 3 scheduler 会维护组件更新 job、pre/post flush callbacks 和当前 flush Promise;Vue 2.7 有 callbacks 队列和环境回退。源码细节随 minor 版本变化,不要把旧版本的 flushSchedulerQueue 名称套到 Vue 3。

16 watchcomputed 的区别与选择

  • computed:从状态得到派生值,默认惰性求值并缓存;
  • watch:监听明确 source,执行异步请求、日志、缓存或第三方同步等副作用;
  • watchEffect:立即执行并自动追踪同步读取的依赖。
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

watch(
  () => route.params.id,
  id => loadArticle(String(id)),
  { immediate: true },
)

要“算出一个值”优先 computed,要“变化后做事情”使用 watch。computed getter 应保持纯净,watch 返回停止监听函数;Vue 3.5 可使用 onWatcherCleanup 取消过期异步请求。Vue 2.7 的 Dep/Watcher 和 Vue 3.5 的 effect/scheduler 不能混为一谈。

17 父子组件创建和挂载顺序

在首次同步挂载、没有异步组件/KeepAlive/特殊 renderer 的前提下,常见顺序是:

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

Vue 3 的 setup() 在组件创建早期执行,Options API 的 created 语义不能直接等同于 setup。更新时通常是父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated,但实际会受组件是否更新、scheduler 和异步 watcher 影响。

异步组件未加载完成时,父组件可能先 mounted,子组件在 loader 完成后再挂载;<Suspense> 可以改变异步依赖的提交时机。不要把这个顺序当作跨所有场景的通信协议。

18 如何缓存组件,缓存后如何更新

使用内置 <KeepAlive>

<KeepAlive :include="['UserList']" :max="10">
  <component :is="activeComponent" />
</KeepAlive>

它缓存组件实例,组件切换出去通常触发 deactivated,切回来触发 activated,不会再次 mounted。只有被淘汰或父级卸载时才真正销毁。

Vue Router 4/5 应缓存 RouterView slot 中的实际路由组件:

<RouterView v-slot="{ Component }">
  <KeepAlive :max="10">
    <component :is="Component" />
  </KeepAlive>
</RouterView>

页面需要重新获取数据时,可以在 onActivated 中刷新,也可以 watch 路由参数:

onActivated(() => {
  loadData()
})

watch(() => route.params.id, () => loadData(), { immediate: true })

include/exclude 匹配组件 name,max 通常按最近最少使用策略淘汰。KeepAlive 是内存和重复初始化之间的取舍,不应缓存所有页面。

19 从零构建一个 Vue 项目

现代 Vue 3 项目通常使用 Vite/create-vue:

pnpm create vue@latest
pnpm install
pnpm dev

可按项目需要选择:

  • Vue Router:路由;
  • Pinia:共享状态;
  • Vitest、Vue Test Utils、Playwright:测试;
  • ESLint、Prettier、TypeScript:质量和类型;
  • vueuse:经过验证的组合式工具;
  • UI 组件库;
  • Axios 或原生 fetch 封装;
  • commitlint、lint-staged、husky:提交检查。

不要为了“完整”把所有依赖都加入项目。Nuxt 4 项目应遵循 Nuxt 的 app/server/public/、路由和自动导入约定,不要照搬纯 Vite 项目的目录。

一个普通 Vite Vue 项目可以是:

src/
├── api/
├── assets/
├── components/
├── composables/
├── layouts/
├── router/
├── stores/
├── utils/
├── views/
├── App.vue
└── main.ts

public 适合无需构建处理的静态资源,src/assets 适合需要被构建器处理的资源;具体目录应服务于边界,而不是为了凑结构。

20 实际工作中的 Vue 最佳实践

编码与可维护性

  • 组件名使用多词,避免与原生 HTML 元素冲突;
  • props、emits 和 slots 明确声明并提供类型;
  • 使用稳定 key,不把 v-ifv-for 写在同一元素;
  • 逻辑复用优先 composable,避免大型 mixin;
  • 组件边界按更新频率和业务职责划分;
  • 让表单、路由和异步状态有明确的 loading/error/empty 状态。

性能

  • 路由和大组件按需加载;
  • 大列表使用虚拟列表、分页或服务端筛选;
  • 稳定 props,避免无意义的父子更新;
  • 对大型不可变数据考虑 shallowRef/markRaw
  • 使用 v-once 前确认内容永远不变;
  • 图片压缩、响应式图片、预加载和懒加载要结合 LCP;
  • 用 Performance 面板和 RUM 测量 LCP、INP、CLS,不要只凭感觉。

安全与可访问性

  • 不使用用户输入拼接模板;
  • 谨慎使用 v-html、动态 URL 和动态 style;
  • 用户内容必须在合适的边界清洗;
  • 交互控件优先使用语义元素,保证键盘焦点和 ARIA;
  • 权限必须由服务端校验,前端隐藏按钮不是安全措施。

21 如何理解 Vuex

Vuex 是集中式状态管理库,核心概念包括 state、getters、mutations 和 actions。它适合维护 Vue 2/4 时期的历史项目:

import { createStore } from 'vuex'

const store = createStore({
  state: () => ({ count: 0 }),
  getters: {
    double: state => state.count * 2,
  },
  mutations: {
    increment(state) {
      state.count++
    },
  },
  actions: {
    increment({ commit }) {
      commit('increment')
    },
  },
})

Vuex 3 对应 Vue 2,Vuex 4 对应 Vue 3;新 Vue 3 项目通常优先 Pinia。Pinia 直接使用 Vue 响应式,提供 state/getters/actions、插件和更好的 TypeScript 体验,没有强制 mutations:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: state => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

简单应用可以用 composable + reactive,不要因为“共享状态”就把所有局部状态塞进全局 store。SSR 应按请求隔离状态,避免用户 A 的数据泄露给用户 B。

View、Action、State 单向循环示意图(已本地化)

22 从 template 到 render 的处理过程

Vue compiler 的核心职责是把 template 转成可执行的 render function:

template
  ↓ parse
AST
  ↓ transform / 优化
带动态信息的 AST
  ↓ codegen
render function

Vue 3 编译器会分析静态节点、动态属性和 block,生成类似这样的结果:

import { createElementVNode as _createElementVNode } from 'vue'

function render(_ctx, _cache) {
  return _createElementVNode('p', null, _ctx.message)
}

真实编译结果会包含 openBlockcreateElementBlock、Patch Flags 和 hoisted nodes。.vue 文件还会经过 SFC 编译器处理 <script setup><template><style scoped>

运行时构建和 Runtime + Compiler 构建的区别在于:前者不在浏览器中编译字符串 template,模板通常在构建阶段编译;后者包含 compiler,能够在运行时编译,但包体积更大。React 也有 JSX/TSX 转换工具和编译优化,但“有没有编译器”不能作为 Vue 与 React 的绝对区别。

23 Vue 实例挂载过程中发生了什么

Vue 3 的入口通常是:

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

createApp(App).mount('#app')

可以从两个方面回答:

  1. 创建和初始化组件实例:解析 props、slots、setup、状态、provide/inject、生命周期和渲染上下文;
  2. 建立渲染更新机制:创建组件 render effect,首次执行 render 得到 VNode,renderer mount 到 DOM;后续响应式状态触发 effect scheduler,重新 render 并 patch。

首次挂载的简化链路:

createApp
  ↓
创建根组件实例
  ↓
执行 setup / 初始化状态
  ↓
创建组件更新 effect
  ↓
首次 render 得到 VNode
  ↓
patch(null, vnode, container)
  ↓
真实 DOM 插入并触发 mounted

如果是 SSR 客户端接管,使用 createSSRApp().mount() 进行 hydration,而不是从空容器重新创建整棵 DOM。hydration 要求服务端和客户端初始渲染结果一致,否则会出现 hydration mismatch。

总结

这篇合集的面试回答可以归纳为:

  • 组件通信优先 props/emits,跨层级使用 provide/inject 或 Pinia;
  • computed 产出派生值,watch/watchEffect 处理副作用;
  • Vue 2 的 defineProperty/Dep/Watcher 与 Vue 3 的 Proxy/effect/scheduler 要分开回答;
  • VNode、Diff、key 和 compiler 优化共同组成渲染更新链路,Virtual DOM 不保证所有场景更快;
  • Router 3 的 addRoutes/next、Vue 2 的生命周期和组件 v-model 不能直接套到 Router 4/5、Vue 3;
  • KeepAlive 缓存实例,RouterView slot、activated/deactivated 和内存淘汰需要一起考虑;
  • 前端权限、输入过滤和 v-html 都不能替代服务端安全边界;
  • 面试源码题应先给公开 API 和行为,再说明版本相关的内部实现。

参考资料

原文作者:杨村长。原合集的 23 个面试主题、回答结构和实战方向予以保留;宣传内容、失效抓取链接、重复段落、损坏代码块及 Vue 2/Vue 3、Router 3/4/5 混淆表述已整理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS