前端学习资料库阅读 · 理解 · 练习

REMEMBER · EXPLAIN · APPLY

Vue 必背知识点

先口述,再对照,最后写代码。
把七天的复习内容,整理成可以直接背的答案。

Vue 3 为主 · 标注版本差异35 张卡 · 32 个必背重点
掌握进度0 / 35

能脱稿讲清楚,再勾选已掌握。

每张卡的复习方法:先用 30–60 秒回答问题,再核对要点;写出最小代码,补上易错点。回答是记忆提纲,面试时要结合自己的项目。

共 35 个知识点

DAY 01 / 07

基础与模板

5 个知识点
01.01必背

Vue 的数据驱动与 MVVM 边界

面试怎么问Vue 怎样把状态变化变成页面变化?为什么常说它借鉴了 MVVM?

查看背诵答案与代码
可以这样回答

Vue 是声明式、组件化的视图框架:先用模板描述状态与页面的关系,渲染时收集响应式依赖,状态变化后调度组件更新,再把新旧虚拟节点的差异应用到真实 DOM。用户操作通过事件修改状态,表单还能用 v-model 简化输入绑定。它借鉴了模型、视图与视图模型的分离思想,但这不代表所有数据都双向流动;组件通信仍以 props 向下传递、emit 向上表达意图为基本约定。

记住这几个点

  • 状态是页面的来源,优先改状态,让渲染器更新页面。
  • 模板负责声明展示关系,事件负责把用户操作转成状态变化。
  • 响应式负责发现依赖变化,渲染器负责更新真实 DOM。
  • v-model 是特定输入场景的绑定约定,组件 props 仍遵守单向数据流。
关键代码 vue
<script setup>
import { ref } from 'vue'
const name = ref('小林')
const editing = ref(false)
</script>

<template>
  <p>你好,{{ name }}</p>
  <input v-if="editing" v-model="name" />
  <button @click="editing = !editing">编辑</button>
</template>
易错点

不能把 MVVM 等同于所有组件数据双向绑定,也不能把响应式系统与虚拟 DOM diff 当成同一个步骤。

常见追问:用了 Vue 后是不是完全不能操作 DOM?

可以在聚焦、测量尺寸或接入第三方库时使用模板引用操作 DOM。业务展示仍应由状态驱动,避免手动改页面内容后又被下一次渲染覆盖。

01.02必背

v-if 与 v-show 的取舍

面试怎么问v-if 和 v-show 有什么区别?你在项目里怎么选?

查看背诵答案与代码
可以这样回答

v-if 决定内容是否真正存在:条件变真时创建,变假时销毁其中的子组件和监听器,初始为假还会跳过创建。v-show 会先渲染元素,再通过 display 控制可见性,组件状态通常保留。我根据创建成本、切换频率和是否需要重置状态选择:频繁展开的轻量面板适合 v-show,低频出现且较重的模块适合 v-if;这只是经验规则,最终还要结合实际开销测量。

记住这几个点

  • v-if 控制创建和销毁,初始条件为假时具有惰性。
  • v-show 切换 display,元素和组件实例仍然存在。
  • v-if 支持 template 分组与 v-else;v-show 不支持这两种写法。
  • 频率只是选型线索,还要考虑内部状态、初始化资源与实际开销。
关键代码 vue
<script setup>
import { ref } from 'vue'
const showHelp = ref(false)
const showEditor = ref(false)
</script>

<template>
  <button @click="showHelp = !showHelp">帮助</button>
  <aside v-show="showHelp">常用操作说明</aside>
  <button @click="showEditor = !showEditor">编辑</button>
  <input v-if="showEditor" placeholder="重新打开会重建" />
</template>
易错点

v-show 隐藏后,组件里的定时器、请求或订阅不会仅因不可见而自动停止。需要按业务控制资源。

常见追问:怎样让切换标签页时保留组件状态?

先明确是否需要持续挂载。简单区域可用 v-show;动态组件可考虑 KeepAlive,但被缓存的组件有激活与停用阶段,资源管理也要对应这些阶段。

01.03必背

v-for、key 与节点身份

面试怎么问列表为什么要用稳定的 key?能不能直接用数组下标?

查看背诵答案与代码
可以这样回答

key 用来辨认哪个新节点对应哪个旧节点。稳定且唯一的 key 表达业务条目的身份,让渲染器在排序、插入和删除时正确复用或移动节点,也让输入内容和组件局部状态跟随正确条目。数组下标表达位置,重排后同一位置可能换了业务对象,容易出现状态错位,所以我通常使用业务 id。改变 key 还可以主动触发组件重建,但不能笼统说加了 key 就一定更快。

记住这几个点

  • key 表达兄弟节点中的稳定身份,业务 id 通常比数组下标合适。
  • 涉及表单值、动画或子组件局部状态时,身份正确尤其重要。
  • key 不能重复,不使用随机值或每次渲染重新生成的值。
  • Vue 3 中同一元素的 v-if 优先于 v-for;先用 computed 筛选,或把条件移到外层。
  • 性能取决于更新模式;稳定身份解决的是复用与状态正确性。
关键代码 vue
<script setup>
import { ref, computed } from 'vue'
const items = ref([
  { id: 'a', title: '响应式', done: false },
  { id: 'b', title: '组件通信', done: true }
])
const pending = computed(() => items.value.filter(x => !x.done))
</script>

<template>
  <label v-for="item in pending" :key="item.id">
    <input v-model="item.done" type="checkbox" />{{ item.title }}
  </label>
</template>
易错点

随机 key 会导致每次更新都被视为新节点;下标 key 在可重排列表里可能复用错误的实例。

常见追问:Vue 3 的 diff 能怎样概括?

先判断节点类型与键能否复用,再更新属性和子节点。带键列表会匹配已有节点并处理增删移动,编译器也会标记动态部分,减少无需比较的内容。

01.04必背

模板表达式与 ref 解包边界

面试怎么问为什么模板里通常不写 .value?哪些位置仍然需要写?

查看背诵答案与代码
可以这样回答

ref 在脚本里通过 .value 读写,模板为了简洁会对渲染上下文的顶层 ref 自动解包。但这有边界:普通对象里嵌套的 ref 参与表达式运算时,不能一概自动解包;reactive 数组和集合中的 ref 元素也需要显式取值。文本插值的最终结果若是 ref,还有额外的便利解包。判断时先区分脚本与模板,再看 ref 位于顶层、普通对象属性还是数组集合元素,避免把几条规则混在一起。

记住这几个点

  • JavaScript 中读写 ref 通常使用 .value。
  • 模板上下文中的顶层 ref 在表达式里会自动解包。
  • 普通对象的嵌套 ref 参与运算时要明确 .value,或先暴露为顶层绑定。
  • 深层 reactive 对象的 ref 属性会解包,reactive 数组和 Map 的 ref 元素不会。
关键代码 vue
<script setup>
import { ref, reactive } from 'vue'
const count = ref(1)
const box = { count: ref(2) }
const list = reactive([ref(3)])
</script>

<template>
  <p>{{ count + 1 }}</p>
  <p>{{ box.count.value + 1 }}</p>
  <p>{{ list[0].value + 1 }}</p>
</template>
易错点

{{ box.count }} 能显示数字,并不代表 {{ box.count + 1 }} 也能正确运算;文本插值最终值解包是另一条规则。

常见追问:v-bind 与双花括号插值有什么区别?

插值用于文本内容,动态属性要用 v-bind 或冒号简写。两者通常接收单个 JavaScript 表达式;复杂分支或有副作用的逻辑应放到计算属性或方法里。

01.05必背

Vue 2 与 Vue 3 的主要变化

面试怎么问从 Vue 2 迁移到 Vue 3,你会重点检查哪些变化?

查看背诵答案与代码
可以这样回答

我会从响应式、组件组织和迁移契约回答。Vue 2 主要靠属性访问器劫持对象属性,对新增属性和部分数组操作存在限制;Vue 3 用 Proxy 处理对象,覆盖范围更完整。Vue 3 提供组合式接口、多个根节点和更好的类型支持,通过 createApp 创建独立应用,组件 v-model 的默认属性与事件约定也改变了。迁移还要检查事件声明和插件用法。Composition API 并非 Vue 3 独占,Vue 2.7 也内置支持。

记住这几个点

  • Vue 2 使用 getter/setter;Vue 3 reactive 对象使用 Proxy,ref 仍使用访问器。
  • Vue 3 使用 createApp 创建应用实例,插件与配置以应用为作用域。
  • Vue 3 支持组件多根节点;多根组件需要明确处理属性透传。
  • 组件 v-model 默认约定从 value/input 变为 modelValue/update:modelValue。
  • Composition API 也内置于 Vue 2.7,不能只凭写法判断版本。
关键代码 js
import { createApp, reactive } from 'vue'
import App from './App.vue'

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

const state = reactive({ title: 'Vue 3' })
state.extra = '新增属性可被追踪'
const tasks = reactive(['响应式'])
tasks[0] = '组件通信'
console.log(state.extra, tasks[0])
易错点

不要说 Vue 3 只能使用 Composition API;Options API 仍然支持。Proxy 也不意味着任意普通局部变量都能被追踪。

常见追问:Vue 3 为什么能减少一些渲染开销?

编译器会分析模板的静态和动态部分,生成静态提升、动态标记及区块结构,让运行时集中处理可能变化的内容。实际收益仍要结合页面结构和更新方式判断。

DAY 02 / 07

响应式与更新

5 个知识点
02.01必背

Proxy、track 与 trigger

面试怎么问Vue 3 响应式是如何收集依赖并触发更新的?

查看背诵答案与代码
可以这样回答

reactive 用 Proxy 拦截对象属性访问。渲染或副作用执行时,读取属性会通过 track 建立当前副作用与属性的订阅关系;属性有效变化后,trigger 找到订阅,通知它们重新计算或进入调度队列。可以用目标对象、属性与订阅集合理解这个过程。实际实现还要处理删除、遍历、数组集合和依赖清理。ref 则通过 .value 的访问器建立相似机制,不能把两者都说成 Proxy 实现。

记住这几个点

  • track 的前提是存在正在收集依赖的 effect;普通读取不等于一定建立订阅。
  • 依赖可以概念化为 target → key → effects,实际内部结构会随版本优化。
  • trigger 通知受影响的订阅;组件更新通常通过调度器批量执行。
  • reactive 返回的是代理,应通过代理读写;直接修改原始对象会绕过拦截。
  • ref 的 .value 使用访问器;当值为对象时,内部仍会转换为 reactive 代理。
关键代码 js
import { reactive, watchEffect, nextTick } from 'vue'
const raw = { count: 0 }
const state = reactive(raw)
watchEffect(() => console.log('计数:', state.count))

async function demo() {
  state.count++ // 通过代理,安排副作用重新执行
  await nextTick() // 本轮日志打印 1
  raw.count++ // 绕过代理,不会安排新执行
  await nextTick() // 不会因这次写入新增日志
  console.log('实际值:', state.count) // 2
}
demo()
易错点

track/trigger 的教学伪代码不是完整源码,不能忽略依赖清理、有效变化判断及不同对象操作的特殊处理。

常见追问:条件分支变了,旧分支的依赖怎么办?

副作用重新执行时需要更新依赖关系,把本轮不再读取的依赖移除。否则旧分支的数据变化仍会产生无用通知,长期运行还可能积累多余订阅。

02.02必背

ref、reactive 与解构/替换边界

面试怎么问ref 和 reactive 怎么选?为什么解构或替换有时会丢响应?

查看背诵答案与代码
可以这样回答

我通常优先用 ref 声明状态,它能承载基本值与对象,整体替换时仍保留同一个容器。reactive 适合保持引用稳定的一组相关属性,但只能处理对象类型;变量改指向新代理,原订阅不会自动跟过去。解构代理的基本值属性会得到普通快照,切断属性访问关系,可以用 toRef、toRefs 或 getter 保留连接。若解构出来的仍是代理对象,修改它的内部属性依然可以响应,不能笼统说所有解构都失效。

记住这几个点

  • ref 支持任意值,对象值默认会转成深层响应式;整体更新用 .value。
  • reactive 支持对象、数组、Map 等,更新时尽量保持代理引用。
  • 解构基本值属性会得到快照;toRef/toRefs 让属性连接以 ref 形式保留。
  • 传给 watch 或 composable 时,用 ref 或 getter 表达会变化的数据源。
  • Vue 3.5 的 defineProps 解构有编译器支持,这是特定语法的例外。
关键代码 js
import { ref, reactive, toRefs } from 'vue'

const profile = ref({ name: '小林' })
profile.value = { name: '小周' } // 容器仍在

const form = reactive({ name: '小林', age: 20 })
const { name, age } = toRefs(form)
name.value = '小周' // 同步修改 form.name
Object.assign(form, { name: '小陈', age: 21 })
console.log(name.value, age.value)
易错点

Object.assign 只覆盖传入属性,不会自动删除旧字段;表单重置要明确完整默认值,不能把它当成任意对象替换。

常见追问:为什么 watch(state.count, callback) 常常不对?

调用时传入的是普通数字,不是可追踪的数据源。可以改成 watch(() => state.count, callback),或用 toRef(state, 'count') 得到属性引用。关键是保留读取属性的连接。

02.03必背

computed 缓存与纯派生状态

面试怎么问computed 和普通方法、watch 的使用场景怎样区分?

查看背诵答案与代码
可以这样回答

computed 描述由已有状态推导出的新值,收集读取的响应式依赖并缓存结果。依赖未变化时多次读取能复用缓存,变化后按需要重新求值。普通方法每次调用都会执行,适合带参数的计算或操作。watch 适合请求、日志和存储等副作用,不应为了同步一个能直接推导的值而额外维护状态。computed 的 getter 要保持纯净,避免修改依赖或发请求;缓存根据响应式依赖失效,不会仅因时间流逝而更新。

记住这几个点

  • computed 表达派生关系,避免同时维护来源与派生结果两份状态。
  • 缓存依据响应式依赖;Date.now() 本身不能触发重新计算。
  • getter 保持纯计算,副作用放到事件处理或 watch 中。
  • 需要双向映射时可提供 get/set,setter 修改来源状态。
关键代码 js
import { ref, computed } from 'vue'

const items = ref([
  { title: 'ref', mastered: false },
  { title: 'computed', mastered: true }
])
const pending = computed(() => {
  return items.value.filter(item => !item.mastered)
})

console.log(pending.value.length)
items.value[0].mastered = true
易错点

不要在 computed getter 里递增响应式“执行次数”,这会引入副作用。调试求值次数可以临时用 console.count。

常见追问:为什么 computed(() => Date.now()) 不会自动走动?

时间函数没有响应式依赖,计算结果不会因为时间过去而自行失效。如果需要时钟,应使用定时器更新一个 ref,并在组件卸载时清理定时器。

02.04必背

watch/watchEffect、清理与 flush

面试怎么问watch 与 watchEffect 有什么区别?怎样避免旧请求覆盖新结果?

查看背诵答案与代码
可以这样回答

watch 显式指定数据源,适合比较新旧值;watchEffect 立即执行,只收集同步阶段读取的依赖,首次 await 之后的读取不会被自动追踪。搜索请求要注册清理,在监听失效或停止时取消旧请求,必要时用请求编号阻止过期结果写入。默认回调在本组件 DOM 更新前,读更新后的页面用 flush: 'post';sync 绕过批处理要谨慎。Vue 3.5 的 onWatcherCleanup 必须在同步阶段注册。

记住这几个点

  • watch 追踪显式 source,不会因回调里读取另一个值就自动监听它。
  • watchEffect 立即执行,只收集同步阶段依赖;第一个 await 是重要边界。
  • onCleanup 可由回调参数取得;onWatcherCleanup 从 Vue 3.5 起支持,必须同步注册。
  • 默认 flush 为 pre;post 适合读取本组件更新后的 DOM;sync 不批量去重。
  • 深度监听可能有遍历成本,嵌套修改时新旧参数也可能引用同一个对象。
关键代码 js
import { ref, watch } from 'vue'
const query = ref('')
const result = ref([])
watch(query, async (value, _, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(value)}`,
      { signal: controller.signal }
    )
    if (!response.ok) throw new Error('请求失败')
    const data = await response.json()
    if (!controller.signal.aborted) result.value = data
  } catch (error) {
    if (error.name !== 'AbortError') console.error(error)
  }
})
易错点

Vue 3.5+ 的 onWatcherCleanup 不能放在 await 之后。回调参数 onCleanup 不受这条同步限制,但仍应尽早注册清理。

常见追问:监听器会自动停止吗?

在 setup 中同步创建并绑定组件的监听器会随组件卸载停止。异步回调里才创建的监听器可能没有这种绑定,应保留停止函数并主动清理。

02.05必背

nextTick 与批量 DOM 更新

面试怎么问状态改了为什么 DOM 还没变?nextTick 保证的是什么?

查看背诵答案与代码
可以这样回答

响应式状态的值通常立即改变,但 DOM 更新会进入调度队列。同一轮同步代码中的多次修改会合并处理,减少重复渲染,所以紧接着读取页面可能得到旧内容。修改状态后 await nextTick,适合聚焦新元素或测量已更新的节点。它保证等待 Vue 当前安排的 DOM 更新完成,不等同于网络请求完成、动画结束或浏览器已经把画面绘制到屏幕。要按实际等待条件选择工具,不能用它修复所有异步时序问题。

记住这几个点

  • 响应式值更新与 DOM 更新不是同一时刻,组件任务通常批量排队。
  • 先改变状态,再 await nextTick,随后读取或操作更新后的 DOM。
  • nextTick 与浏览器绘制、动画结束、接口返回分别属于不同等待条件。
  • 连续同步修改通常合并;flush: 'sync' 的监听器是需要单独考虑的例外。
关键代码 vue
<script setup>
import { ref, nextTick } from 'vue'
const editing = ref(false)
const inputEl = ref(null)
async function startEditing() {
  editing.value = true
  await nextTick()
  inputEl.value?.focus()
}
</script>
<template>
  <button @click="startEditing">开始编辑</button>
  <input v-if="editing" ref="inputEl" />
</template>
易错点

setTimeout(fn, 0) 并不是 nextTick 的语义替代;它安排的是定时器任务,无法准确表达“等待 Vue 的本次更新”。

常见追问:等待浏览器绘制或 CSS 动画结束该怎么做?

按真正需要的条件等待:下一帧调度可用 requestAnimationFrame,过渡结束使用相关事件或组件钩子。nextTick 仅负责 Vue 更新,不包含这些后续阶段。

DAY 03 / 07

组件与组合式 API

5 个知识点
03.01必背

props/emits 与单向数据流

面试怎么问子组件为什么不应该直接修改 props?组件事件会冒泡吗?

查看背诵答案与代码
可以这样回答

props 是父组件给子组件的输入,状态所有权应留在父组件;子组件需要修改时通过 emit 表达意图,由父组件决定是否更新,变化来源更容易追踪。props 绑定只读,但对象或数组属性的内部成员仍可能被修改,能做到不代表适合作为默认通信方式。组件自定义事件不会像原生事件向祖先自动冒泡。声明 emits 能明确公共契约,也避免事件监听器被错误当作普通透传属性,维护组件边界。

记住这几个点

  • props 向下传输入,emit 向上报告意图,状态拥有者负责真正更新。
  • props 只读主要约束属性绑定;嵌套对象共享引用可能被子组件修改。
  • 自定义组件事件只由直接监听该组件的使用者接收,不自动冒泡。
  • Vue 3.5+ 会在同一 script setup 内转换 defineProps 的解构访问;跨函数传递仍推荐 getter。
  • Vue 3.4 及更早版本,直接解构 props 的基本值没有这项编译支持。
关键代码 vue
<script setup>
import { computed } from 'vue'
const props = defineProps({ title: String, done: Boolean })
const emit = defineEmits(['toggle'])
const label = computed(() => props.done ? '已掌握' : '待复习')
</script>

<template>
  <button @click="emit('toggle')">
    {{ props.title }}:{{ label }}
  </button>
</template>
易错点

不能笼统说“解构 props 一定丢响应”;Vue 3.5+ 的 defineProps 解构有特定编译支持,但 watch(title, ...) 仍是传值,应改成 getter。

常见追问:子组件需要编辑表单,但点取消不能影响父组件怎么办?

在子组件建立明确的本地草稿,必要时对嵌套结构复制;提交时发出完整结果,取消时丢弃草稿。还要明确父输入变化时是否刷新草稿,避免静默覆盖编辑。

03.02必背

组件 v-model 与 defineModel

面试怎么问组件 v-model 的底层约定是什么?defineModel 是怎么简化它的?

查看背诵答案与代码
可以这样回答

组件 v-model 可以拆成向下的属性和向上的更新事件。Vue 3 默认用 modelValue 与 update:modelValue,父组件收到事件后修改自己的状态;带参数时对应指定名称,也支持多个模型。Vue 3.4 起推荐 defineModel 编译宏,它生成这些声明并返回可读写的 ref,简化子组件代码。底层依然遵循属性与事件契约,状态所有权仍需明确。子端配置默认值不会自动初始化父端,父值缺失时要防止两端不同步。

记住这几个点

  • 默认契约是 modelValue 属性 + update:modelValue 事件。
  • v-model:title 对应 title 属性 + update:title 事件,可同时绑定多个模型。
  • defineModel 是 script setup 编译宏,Vue 3.4+ 推荐使用,不需要从 vue 导入。
  • 修改 defineModel 返回的 ref 会通过模型契约更新父组件绑定值。
  • 子端设置 default 而父端仍为 undefined,可能造成初始值不同步。
关键代码 vue
<!-- 子组件 TitleInput.vue,Vue 3.4+ -->
<script setup>
const title = defineModel('title', {
  type: String,
  required: true
})
</script>

<template>
  <input v-model="title" />
</template>
<!-- 父组件使用:<TitleInput v-model:title="title" /> -->
易错点

defineModel 的 default 不是父组件状态初始化。父端没提供值时,子端默认值可能与父端 undefined 不一致。

常见追问:Vue 3.4 之前怎么实现同样的组件?

显式声明对应 prop 和 update 事件:输入框的 value 读取 prop,input 回调 emit 新值。父组件的 v-model 会把这两部分连接起来,无需在子组件修改 prop。

03.03必背

普通、具名与作用域插槽

面试怎么问插槽与 props 的区别是什么?作用域插槽的数据方向怎样理解?

查看背诵答案与代码
可以这样回答

props 主要传数据或配置,slot 让父组件决定一段内容怎样渲染,子组件负责放到指定位置。默认插槽接收未命名内容,具名插槽区分标题、正文等区域。插槽表达式使用定义它的父级作用域;若渲染需要子组件内部数据,子组件通过 slot props 传出,父组件用作用域插槽接收。我把它理解为子组件调用父组件提供的渲染函数,并传入本次所需数据。父子作用域没有合并,公开的数据边界依然清楚。

记住这几个点

  • 默认插槽用于未命名内容;具名插槽用 #name 或 v-slot:name 选择出口。
  • 插槽内容使用父作用域,不能直接读取子组件未传出的内部变量。
  • 作用域插槽通过 slot props 把子端数据提供给父端渲染。
  • 无插槽内容时,出口可使用后备内容;数据行为与展示可因此分离。
关键代码 vue
<!-- 子组件 QuestionList.vue -->
<script setup>
defineProps({ items: { type: Array, default: () => [] } })
</script>
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot name="row" :item="item">{{ item.title }}</slot>
    </li>
  </ul>
</template>
<!-- 父端使用 #row="{ item }" 自定义每行内容 -->
易错点

作用域插槽并没有把父子组件变量合并成同一个作用域。只能通过传出的 slot props 读取子端提供的数据。

常见追问:什么时候会用无渲染组件?

当多个界面共享一套状态和行为,但展示结构差异很大时,可通过作用域插槽暴露状态与操作。若无需组件边界和插槽结构,composable 往往也能复用同样逻辑。

03.04进阶

provide/inject 与状态所有权

面试怎么问provide/inject 适合解决什么问题?注入值一定响应式吗?

查看背诵答案与代码
可以这样回答

provide/inject 解决多层组件逐级转交依赖的问题:祖先按键提供上下文,后代读取最近祖先的同名提供值,适合主题、表单上下文和组件库协作。机制本身不会把普通值自动变成响应式,要提供 ref 或 reactive 保持连接。我让提供方持有状态与修改函数,后代拿 readonly 状态和明确操作,减少隐式修改。大型项目用共享 Symbol 键避免冲突,也要明确缺少提供者时使用默认值还是报错。

记住这几个点

  • 后代按注入键读取最近祖先的提供值,减少层层透传。
  • 传递 ref/reactive 才能保留对应响应式能力,注入的 ref 不自动解包。
  • 状态修改集中在 provider,暴露操作函数,必要时用 readonly 约束消费方。
  • 导出共享 Symbol 作为键,避免同名字符串碰撞;缺少 provider 时应明确默认值或报错。
关键代码 js
// theme-context.js;两个函数分别由祖先和后代 setup 调用
import { ref, readonly, provide, inject } from 'vue'
export const themeKey = Symbol('theme')
export function provideTheme() {
  const theme = ref('light')
  function toggle() {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  provide(themeKey, { theme: readonly(theme), toggle })
}
export function useTheme() {
  const context = inject(themeKey)
  if (!context) throw new Error('缺少主题提供方')
  return context
}
易错点

祖先 provide('count', count.value) 传的是当时的普通值,不是 ref;后代不会因此持续收到 count 的更新。

常见追问:provide/inject 和全局状态库怎么选?

前者适合组件子树内的上下文与依赖,范围随提供者确定;跨页面共享业务状态、需要统一调试和状态组织时可以用 Pinia。先按状态所有者与共享范围选择。

03.05必背

Composition API 与 composable

面试怎么问Composition API 带来什么好处?怎样写一个可复用的 composable?

查看背诵答案与代码
可以这样回答

Composition API 让同一业务关注点的状态、派生值和副作用靠近组织,封装成 composable 后,不同组件能显式引入,来源和依赖更清楚。组合函数通常返回包含多个 ref 的普通对象,解构后仍保留响应式。函数内创建状态时每次调用独立,模块顶层状态则共享。如果注册生命周期或依赖注入,需要在组件上下文中按要求同步调用;定时器、订阅等副作用还要清理,不能只复用初始化代码而忽略释放。

记住这几个点

  • 按业务关注点组织逻辑,复用时明确输入、返回值和副作用。
  • 通常返回普通对象包裹 refs,方便解构且不丢失引用连接。
  • 函数内部创建的状态通常按调用独立;模块顶层状态由所有调用者共享。
  • 监听、定时器和事件需对应清理;依赖组件上下文的 API 按上下文要求调用。
  • Options API 仍可用;组合式写法不是函数式编程,也不是 React Hooks 的重渲染模型。
关键代码 js
import { ref, computed } from 'vue'
export function useReviewCounter(initial = 0) {
  const completed = ref(initial)
  const hasProgress = computed(() => completed.value > 0)
  function increment() { completed.value++ }
  function reset() { completed.value = initial }
  return { completed, hasProgress, increment, reset }
}

// 在组件 setup 中调用;两次调用各自持有状态
const { completed, increment } = useReviewCounter()
increment()
易错点

把 ref 放在模块顶层会变成共享状态,尤其在服务端渲染中不能无意让不同请求共用用户状态。

常见追问:组合函数接收会变化的 prop,怎样避免只拿到快照?

传入 ref 或 getter,让函数在 computed/watchEffect 等追踪作用域里读取。可以使用 toValue 规范化输入,但要在副作用内部调用,才能收集 getter 中读取的依赖。

DAY 04 / 07

路由与状态管理

5 个知识点
04.01必背

hash、history 与刷新 404

面试怎么问Vue Router 的 hash 和 history 模式有什么区别?为什么 history 刷新会 404?

查看背诵答案与代码
可以这样回答

hash 模式把路由放在井号后,这部分不会发送给服务器,部署配置较简单。history 模式用正常路径和浏览器历史 API,直接访问子路径时服务器必须返回应用入口,再由前端匹配路由。部署时要让真实静态文件和接口正常处理,只对应用路由回退到 index.html,并在前端配置未匹配路由页面。

记住这几个点

  • hash 片段不参与 HTTP 请求路径
  • history 子路由需要服务器回退
  • 静态资源与 API 保留各自处理规则
  • 前端 404 页面与 HTTP 404 状态是两件事
关键代码 js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', component: () => import('./Home.vue') },
    { path: '/:pathMatch(.*)*',
      component: () => import('./NotFound.vue') },
  ],
})
// 服务器另行配置应用路由回退。
易错点

把所有请求一律返回 index.html,会让丢失的 JS 文件或 API 请求收到 HTML,掩盖部署错误。

常见追问:history 模式部署到 /app/ 子目录要注意什么?

构建资源基础路径、createWebHistory 的 base 和服务器回退路径要一致。还要验证直接访问子路由、刷新、静态资源与后退操作。

04.02必背

导航守卫与权限边界

面试怎么问如何用导航守卫实现登录控制?它能保证权限安全吗?

查看背诵答案与代码
可以这样回答

导航守卫控制一次路由导航能否继续,可以返回 false 取消,返回路由地址重定向,也可以等待异步结果。常用全局前置守卫检查登录状态和路由元信息,并避免登录页循环重定向。前端守卫负责页面体验,接口必须在服务端验证身份和资源权限;隐藏按钮和阻止跳转都不能替代服务端授权。

记住这几个点

  • 守卫可同步或异步返回结果
  • 用路由 meta 声明页面访问要求
  • 避免重复调用 next 或循环重定向
  • 服务端逐次验证接口与资源权限
关键代码 js
router.beforeEach(async (to) => {
  if (!to.meta.requiresAuth) return
  const allowed = await session.checkLogin()
  if (!allowed && to.name !== 'login') {
    return { name: 'login' }
  }
})
// session 为项目的会话服务。
// 后端接口仍需独立检查权限。
易错点

直接相信 localStorage 中的角色或登录标记,既不能证明身份,也不能防止用户绕过前端调用接口。

常见追问:beforeEach 与 beforeResolve 有什么区别?

beforeEach 更早执行;beforeResolve 在组件内守卫和异步路由组件解析后、导航确认前执行,适合只在确定能进入页面后做必要的数据准备。

04.03必背

route、router 与路由组件复用

面试怎么问route 和 router 有什么区别?参数变化为什么不重新执行 mounted?

查看背诵答案与代码
可以这样回答

route 是当前路由的响应式信息,包含 params、query、path 等;router 是执行 push、replace 等导航的路由器实例。同一路由记录仅参数变化时,通常会复用组件实例,所以不能只在 mounted 请求数据。应监听具体参数,或使用路由更新守卫处理变化,并对首次加载和异步请求竞态作明确处理。

记住这几个点

  • useRoute 读取位置,useRouter 执行导航
  • 参数变化可能复用现有组件
  • 监听具体字段,避免监听整个 route
  • watch 的 immediate 可处理首次数据加载
关键代码 js
import { watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
watch(() => route.params.id, (id) => {
  console.log('当前用户 ID:', id)
}, { immediate: true })

function openUser(id) {
  return router.push({ name: 'user', params: { id } })
}
易错点

watch(route.params.id, ...) 把当时的字符串传给 watch,不能跟踪参数变化;应使用 getter。

常见追问:什么时候使用路由 props?

当页面组件只需要某些参数时,把参数映射为 props 可以减少组件对路由器的直接依赖,让组件更容易复用和独立测试。

04.04必背

Pinia 的状态与安全解构

面试怎么问Pinia 的 state、getters、actions 分别是什么?如何保持解构后的响应式?

查看背诵答案与代码
可以这样回答

Pinia 用 state 保存共享状态,getters 表示基于状态的派生值,actions 封装业务操作并可执行异步任务。store 本身是响应式对象,直接解构其中的普通状态值会丢失与 store 的响应式关联。需要解构状态和 getter 时用 storeToRefs;action 可以直接解构。只有确实跨组件共享的状态才需要放入 store。

记住这几个点

  • state 是数据,getter 是派生值,action 是业务操作
  • storeToRefs 提取响应式状态和 getter
  • action 可以直接解构调用
  • Pinia 允许直接修改状态,也支持 $patch
关键代码 js
import { defineStore, storeToRefs } from 'pinia'

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

const store = useCounter()
const { count, double } = storeToRefs(store)
const { increment } = store
increment()
易错点

不要说 Pinia 必须通过 action 修改状态;这是团队可以制定的约定,不是框架限制。

常见追问:Setup Store 需要注意什么?

返回的 ref 对应 state、computed 对应 getter、函数对应 action。所有状态应返回供 Pinia 识别,隐藏状态可能破坏 SSR、开发工具和插件工作。

04.05必背

状态持久化与恢复

面试怎么问Pinia 数据刷新后会保留吗?怎样设计持久化?

查看背诵答案与代码
可以这样回答

Pinia 的内存状态默认会随页面刷新重建,持久化需要额外实现。我会只保存偏好、草稿等必要字段,启动时读取、解析并校验版本和结构,再恢复到 store;后续通过订阅保存变化。读写要处理存储不可用和损坏数据,避免每次输入都写入大对象。密码和敏感凭据不放在前端持久化存储中。

记住这几个点

  • 默认内存状态不等于持久化
  • 按字段白名单保存,加入版本号
  • 恢复前做 JSON 解析与数据结构校验
  • SSR 中只在客户端访问浏览器存储
易错点

直接把整个 store 序列化,可能保存临时状态、过期接口数据和不应持久化的信息。

常见追问:$subscribe 与普通 watch 有什么差别?

Pinia 的订阅带有变更类型等信息,并能在一次函数式 $patch 后集中通知。在组件 setup 中注册的订阅默认随组件卸载解除;组件外或 detached 订阅应明确释放位置。

DAY 05 / 07

生命周期与异步

5 个知识点
05.01必背

生命周期与 DOM 时机

面试怎么问Vue 3 生命周期怎么用?什么时候可以操作 DOM?

查看背诵答案与代码
可以这样回答

setup 用于建立状态和同步注册生命周期,onMounted 在组件 DOM 创建并插入、同步子组件挂载后调用,可访问模板引用。更新状态后 DOM 不会立即完成更新,读取本次结果可以等待 nextTick。onUnmounted 适合释放定时器和事件监听。异步组件不保证随父组件一起完成挂载,更新钩子里也应避免无条件修改状态。

记住这几个点

  • 生命周期钩子在 setup 调用链中同步注册
  • mounted 不等于所有异步内容都已完成
  • 等待本轮 DOM 更新用 nextTick
  • 获取外部资源时就设计对应释放逻辑
关键代码 vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const width = ref(0)
const measure = () => { width.value = window.innerWidth }
onMounted(() => {
  measure()
  window.addEventListener('resize', measure)
})
onUnmounted(() => {
  window.removeEventListener('resize', measure)
})
</script>
<template><p>宽度:{{ width }}</p></template>
易错点

在 setTimeout 或普通异步回调里才调用 onMounted,通常已失去当前组件上下文,无法正确注册。

常见追问:父子组件的 mounted 和 updated 顺序怎么理解?

父组件 mounted 等同步子组件挂载后执行;父组件 updated 在子组件 updated 后执行。异步组件和 Suspense 下的组件不纳入同样的同步挂载保证。

05.02必背

异步请求竞态与过期结果

面试怎么问连续搜索时,旧请求覆盖新结果怎么解决?

查看背诵答案与代码
可以这样回答

连续请求可能按相反顺序完成,最后到达的结果不一定对应当前输入。我会在参数变化时取消旧请求,并用请求序号或当前参数校验,保证只有仍有效的请求可以提交结果。错误和 loading 状态同样需要有效性检查;取消请求通常不作为业务错误展示。防抖能减少请求数量,但不能单独解决已经发出的请求竞态。

记住这几个点

  • 区分发起顺序与完成顺序
  • AbortController 可取消支持 signal 的请求
  • 请求序号控制结果、错误与 loading 提交
  • 组件离开或参数变化时处理旧请求
关键代码 js
import { ref } from 'vue'
const result = ref(null)
const error = ref('')
let version = 0
async function search(keyword) {
  const current = ++version
  try {
    const response = await fetch('/api/search?q=' + encodeURIComponent(keyword))
    if (!response.ok) throw new Error('请求失败')
    const data = await response.json()
    if (current === version) result.value = data
  } catch (e) {
    if (current === version) error.value = e.message
  }
}
易错点

只保护 data 而不保护 loading 和 error,旧请求的 finally 仍可能提前关闭新请求的加载提示。

常见追问:既然可以取消,为什么还考虑结果有效性判断?

部分异步任务不可取消,取消也可能发生在响应到达以后。有效性判断明确限制谁可以提交状态,能覆盖请求取消之外的异步工作。

05.03必背

watch 的副作用清理

面试怎么问watch 发请求或启动定时器,怎么清理上一轮副作用?

查看背诵答案与代码
可以这样回答

watch 每轮执行产生的请求、定时器等资源,应注册清理函数,在这一轮失效时释放。Vue 3.5 起可以使用 onWatcherCleanup,但必须在回调同步阶段、第一次 await 之前注册。也可以使用 watch 第三个参数 onCleanup。同步创建于组件 setup 的侦听器会随组件卸载停止,异步创建的侦听器需要自行管理停止。

记住这几个点

  • 清理上一轮资源,防止重复工作或过期写入
  • onWatcherCleanup 要求 Vue 3.5+
  • onWatcherCleanup 不能在 await 后注册
  • 异步创建 watcher 时保存并调用停止函数
关键代码 js
import { ref, watch, onWatcherCleanup } from 'vue'
const id = ref(1)
const user = ref(null)
watch(id, async (value) => {
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())
  try {
    const response = await fetch('/api/users/' + value, { signal: controller.signal })
    if (!response.ok) throw new Error('加载失败')
    const data = await response.json()
    if (!controller.signal.aborted) user.value = data
  } catch (e) {
    if (e.name !== 'AbortError') console.error(e)
  }
}, { immediate: true })
易错点

onWatcherCleanup 写在 await fetch() 之后已不满足同步注册要求;必须先注册,再开始等待。

常见追问:回调参数 onCleanup 也有同样的同步限制吗?

参数 onCleanup 绑定于当前侦听器实例,没有 onWatcherCleanup 的同样同步限制。不过请求发出前及时注册清理,仍能避免等待期间遗漏资源释放。

05.04必背

KeepAlive 的缓存与激活

面试怎么问KeepAlive 保留什么?切走页面后为什么定时器还在运行?

查看背诵答案与代码
可以这样回答

KeepAlive 缓存组件实例及其状态。组件切走时进入停用状态,通常没有卸载,所以不能依赖 onUnmounted 暂停后台任务。我会在 onActivated 恢复需要的工作,在 onDeactivated 暂停轮询或监听,并保证重复激活不会重复注册。缓存可用 include、exclude 和 max 控制,max 超限时会淘汰最久未访问的实例。

记住这几个点

  • 停用不等于卸载,实例状态仍在
  • activated 首次挂载也会执行
  • deactivated 停用和卸载时均可能执行
  • 限制缓存数量,避免长期保留大量页面
关键代码 js
import { onActivated, onDeactivated } from 'vue'
let timer
function stop() {
  clearInterval(timer)
  timer = undefined
}
onActivated(() => {
  stop()
  timer = setInterval(() => console.log('轮询'), 5000)
})
onDeactivated(stop)
易错点

KeepAlive 不是自动暂停业务逻辑的开关;自己创建的 interval 或事件订阅需要明确暂停。

常见追问:include / exclude 根据路由 path 还是组件名称匹配?

根据组件 name 匹配。使用 script setup 的单文件组件在 Vue 3.2.34 起可从文件名推断名称;需要精确控制时应明确检查最终组件名称。

05.05进阶

异步组件与 Suspense 边界

面试怎么问defineAsyncComponent 与 Suspense 各解决什么问题?

查看背诵答案与代码
可以这样回答

defineAsyncComponent 把组件加载器包装为可渲染组件,可配合动态 import 实现按需加载,并配置加载、错误和超时状态。Suspense 用统一边界协调组件树中的异步 setup 和受其管理的异步组件,显示 fallback。它不会自动等待普通请求,也不自带完整错误边界。目前官方仍将 Suspense 标为实验特性。

记住这几个点

  • 异步组件负责组件代码加载
  • Suspense 管理特定的异步依赖
  • 普通 onMounted 请求不自动成为依赖
  • 错误可由父组件 onErrorCaptured 等机制处理
易错点

Vue Router 的路由动态 import 不等同于 defineAsyncComponent,也不会仅因路由懒加载就自动触发 Suspense。

常见追问:Suspense 包裹异步组件时,组件自己的 loading 配置还生效吗?

默认受 Suspense 管理的异步组件由边界控制加载状态,自己的 loading、error、delay、timeout 选项会被忽略;可设 suspensible: false 退出该控制。

DAY 06 / 07

性能与工程

5 个知识点
06.01必背

编译、渲染与更新流程

面试怎么问从 Vue 模板到页面更新,大致经过哪些步骤?

查看背诵答案与代码
可以这样回答

模板先编译为 render 函数,通常在构建时完成。挂载时执行 render,收集使用的响应式依赖并生成虚拟节点树,再由渲染器创建真实 DOM。依赖变化后组件更新任务进入调度队列,重新生成虚拟节点并比较新旧结果,更新必要 DOM。编译器还会标记动态部分,使运行时减少无需处理的比较工作。

记住这几个点

  • 模板编译产生 render 函数
  • 首次渲染创建 DOM 并收集依赖
  • 状态更新经过调度与 patch
  • 静态缓存和 patch flags 减少运行时工作
易错点

不要把虚拟 DOM 描述为天然比直接操作 DOM 更快;它提供声明式更新和可组合的渲染模型,性能取决于具体工作量。

常见追问:为什么 Vue 模板容易做编译优化?

模板结构相对确定,编译器能分析哪些节点和属性会变化,并给运行时提供提示。手写任意 render 函数时,很多静态信息难以提前判断。

06.02必背

组件更新与 props 稳定

面试怎么问父组件更新会让所有子组件都更新吗?怎样减少列表更新?

查看背诵答案与代码
可以这样回答

父组件更新不代表每个子组件都必须执行一次完整更新,子组件自身依赖变化、接收的 props 变化等会影响是否更新。优化列表时,我会把子项真正需要的状态传下去,例如由父组件计算每项的 active 布尔值,让大多数子项的 props 保持稳定;同时避免每次渲染都生成没有必要的新对象引用。

记住这几个点

  • 先判断更新原因,再决定优化方式
  • 把共享 activeId 转成每项 active 状态
  • 避免无意义的新对象或数组 props
  • 组件内部响应式状态变化仍可触发更新
关键代码 vue
<script setup>
import { ref } from 'vue'
import ListItem from './ListItem.vue'
const activeId = ref(1)
const items = ref([{ id: 1 }, { id: 2 }])
</script>
<template>
  <ListItem v-for="item in items" :key="item.id"
    :id="item.id" :active="item.id === activeId" />
</template>
易错点

v-memo 的依赖漏写可能让界面不更新;不能为了减少渲染随意冻结需要变化的 UI。

常见追问:什么时候考虑 v-once 或 v-memo?

v-once 适合挂载后完全不再变化的内容;v-memo 适合已测出瓶颈且能完整声明变化依赖的区域。先稳定数据与 props,再使用这些局部优化。

06.03必背

性能测量与虚拟列表

面试怎么问一个 Vue 页面很卡,你会怎么定位和优化?

查看背诵答案与代码
可以这样回答

我会先确定卡在首屏加载、交互响应还是持续内存增长,用浏览器性能面板观察网络、脚本、布局和渲染耗时,再定位最重的组件或任务。大量列表节点造成卡顿时,虚拟列表只渲染可视区域附近的项,减少 DOM 数量;还要处理高度、滚动和焦点。每项优化都用同一场景前后测量,避免只凭感觉判断。

记住这几个点

  • 先建立可重复的慢操作场景
  • 区分下载、执行、更新与布局成本
  • 虚拟列表减少实际 DOM,不减少原始数据
  • 记录优化前后耗时与内存证据
易错点

把数据简单截取前 100 条不等于虚拟列表,会丢失后续内容;虚拟列表要根据滚动位置切换渲染范围。

常见追问:虚拟列表有什么实现难点?

需要维护内容总高度、可视索引和占位偏移,动态行高还需要测量与校正。还要验证键盘导航、焦点保留、滚动定位和可访问性。

06.04必背

懒加载、代码分割与 tree shaking

面试怎么问路由懒加载和 tree shaking 有什么区别?

查看背诵答案与代码
可以这样回答

懒加载把暂时不需要的代码延后获取,动态 import 可让构建工具生成按需加载的代码块。tree shaking 根据模块静态结构去掉未使用的导出,目标是减少最终代码。两者分别改变加载时机和代码体积。优化时还要检查依赖是否支持静态分析、包是否有副作用,并用生产构建分析结果确认收益。

记住这几个点

  • 代码分割不等于删除代码
  • tree shaking 依赖可分析的模块结构
  • 页面组件可以使用路由动态 import
  • 验证首屏请求和生产包体积
关键代码 js
const routes = [
  {
    path: '/reports',
    name: 'reports',
    component: () => import('./views/Reports.vue'),
  },
]
// Vue Router 直接接收动态 import 加载函数。
// 无需为路由组件再套 defineAsyncComponent。
易错点

导入整个大包后只调用一个方法,不代表一定能删除其余代码;还要看包的模块格式、副作用和实际构建结果。

常见追问:所有小组件都拆成异步组件合适吗?

拆分过细会增加调度和加载复杂度,弱网下也可能增加等待。优先拆分重页面和不常使用的大功能,依据实际加载链和使用频率确定边界。

06.05进阶

SSR 与 hydration 的边界

面试怎么问SSR 是什么?hydration 不一致常见原因有哪些?

查看背诵答案与代码
可以这样回答

SSR 在服务端把组件渲染为 HTML,客户端 hydration 再连接组件状态与事件,让页面可交互。服务端和客户端初次输出应一致;随机数、时区差异和无效 HTML 嵌套可能造成不一致。浏览器 API 放到客户端阶段使用,服务端共享状态应按请求创建,避免用户之间的数据串用。SSR 也增加了服务端运行和部署成本。

记住这几个点

  • 服务端输出 HTML,客户端恢复交互
  • 首次渲染数据与结构必须协调
  • window 和 DOM API 不在服务端直接访问
  • 按请求创建应用和用户状态
易错点

把用户状态放在服务端模块单例中,可能跨请求共享;这不仅是 hydration 问题,还可能导致数据泄漏。

常见追问:SSR 中 onMounted 会执行吗?

onMounted 等 DOM 相关生命周期不会在服务端执行。依赖浏览器的操作放在客户端钩子;不要在服务端 setup 中启动依赖卸载钩子清理的长期定时器。

DAY 07 / 07

面试与项目表达

5 个知识点
07.01必背

Vue 2 限制与 Vue 3 迁移

面试怎么问Vue 2 响应式有哪些限制?迁移 Vue 3 时要看什么?

查看背诵答案与代码
可以这样回答

Vue 2 基于属性 getter 和 setter,无法侦测对象属性新增删除,以及数组下标赋值和 length 修改,需要 Vue.set、Vue.delete 或 splice 等方式。Vue 3 的 reactive 基于 Proxy,解决这些拦截限制。迁移还要检查应用创建、组件 v-model、事件、生命周期和插件兼容性,不能只升级依赖版本就认为完成。

记住这几个点

  • Vue 2 对象新增删除需要特殊处理
  • Vue 2 数组索引和长度修改有限制
  • Vue 3 不再需要 Vue.set / Vue.delete
  • 迁移按功能回归并核对第三方插件
易错点

Vue 3 支持属性新增,不表示普通对象自动响应式;仍要通过 reactive、ref 等建立响应式状态。

常见追问:Vue 2 项目可以一次性改成 Composition API 吗?

迁移不要求重写所有组件,Vue 3 仍支持 Options API。可以先解决不兼容 API 和生态依赖,再根据维护价值逐步拆分复用逻辑。

07.02必背

内存泄漏与资源所有权

面试怎么问Vue 会自动清理所有资源吗?怎样排查内存泄漏?

查看背诵答案与代码
可以这样回答

Vue 会管理组件自身的渲染和同步创建的响应式副作用,但自己创建的定时器、全局监听、外部实例和长期订阅仍需要释放。我会明确资源由谁创建、何时暂停和销毁,缓存页面还要处理停用。排查时重复进入退出同一页面,观察内存是否持续增长,再用堆快照查看残留 DOM 和对象的引用链。

记住这几个点

  • 外部资源必须有对应清理动作
  • 注册和移除监听使用同一函数引用
  • 缓存停用与真正卸载分别处理
  • 稳定重复场景后比较堆快照
易错点

removeEventListener('resize', () => ...) 使用新的函数对象,不能移除之前注册的匿名监听。

常见追问:为什么异步创建 watcher 要特别小心?

它未必自动绑定到组件的同步 setup 生命周期。需要保存 watch 返回的 stop,在拥有者销毁时调用;优先同步创建并用条件控制执行内容。

07.03必背

权限、模板与前端安全

面试怎么问Vue 自动转义就没有 XSS 风险了吗?权限如何保证?

查看背诵答案与代码
可以这样回答

Vue 的文本插值和普通属性绑定默认会转义内容,但不意味着所有输入都安全。v-html 会把字符串作为 HTML 注入,不能直接使用不可信内容;也不能把用户内容作为可编译模板。URL 等数据要按用途校验。页面权限只改善前端展示,真正的数据读取和修改权限必须由服务端校验,避免只靠隐藏按钮保护接口。

记住这几个点

  • 插值转义不覆盖所有注入场景
  • 不可信内容不作为模板执行
  • 富文本经过适当清洗后才能渲染
  • 服务端验证身份、操作和资源归属
关键代码 vue
<script setup>
import { ref } from 'vue'
const comment = ref('<img src=x onerror=alert(1)>')
</script>
<template>
  <!-- 插值作为文本显示,不执行其中的 HTML -->
  <p>{{ comment }}</p>
</template>
易错点

把来自接口的 HTML 直接传给 v-html,接口返回不代表内容可信;内容仍可能来自用户或第三方。

常见追问:过滤脚本标签就能安全渲染富文本吗?

不够,事件属性、危险 URL 和复杂 HTML 解析场景仍可形成风险。应使用适合场景且持续维护的清洗方案,并保留服务端验证与防护。

07.04必背

组件测试与异步验收

面试怎么问Vue 组件应该测试什么?怎样避免脆弱测试?

查看背诵答案与代码
可以这样回答

组件测试应围绕用户能观察到的行为:给定 props 和状态,点击或输入后,页面和发出的事件是否正确。重点覆盖加载、成功、失败、边界和异步竞态,避免依赖组件私有变量或方法名。纯逻辑可单独做单元测试,登录到提交等跨页面关键流程用端到端测试,样式和真实浏览器行为也需要相应环境验证。

记住这几个点

  • 按输入、操作和可观察输出组织用例
  • 验证 props、事件、插槽及条件界面
  • 异步测试等待实际完成条件
  • 避免只有快照或只断言内部实现
易错点

测试只断言调用了某个私有方法,方法正确但界面坏了仍可能通过;应验证用户看到的结果和公开事件。

常见追问:组件测试与端到端测试如何分工?

组件测试快速验证一个组件及其交互;端到端测试在真实浏览器走完整流程,覆盖路由、状态管理、资源和请求集成。按失败影响选择关键流程。

07.05必背

把知识点讲成项目经验

面试怎么问面试问你做过什么 Vue 优化,怎样回答更具体?

查看背诵答案与代码
可以这样回答

我会先说明真实场景和问题影响,再说如何测量并定位原因,然后解释方案、取舍和验证结果。例如列表操作卡顿,要讲清数据规模、耗时证据、更新来源、采用的优化及前后测量。没有真实数字就诚实说明观察方法,不编造收益;最后补充错误路径、回归范围和仍然存在的限制,体现完整判断过程。

记住这几个点

  • 场景:用户在哪一步受到什么影响
  • 证据:怎么复现并定位瓶颈
  • 方案:为什么选择及放弃什么做法
  • 结果:验证效果、边界与回归情况
易错点

只罗列用了 Pinia、懒加载、缓存等名词,无法说明这些选择解决了项目中的哪个实际问题。

常见追问:被追问优化没有收益时怎么回答?

说明原假设、测量结果和排除原因,再给出调整方向。能诚实解释无效尝试和证据,比把每种技术都描述为显著提升更可信。