Search articles

页面交互卡顿、掉帧严重怎么排查?从帧预算出发,用 Chrome Performance 定位脚本、布局、绘制与合成瓶颈,再选择任务拆分、虚拟列表和绘制优化,并通过同场景录制验证效果。Vue 面试完整笔记:响应式、组件、路由、Pinia 与渲染机制覆盖《Vue 面试》合集全部 28 集:前言与 27 节专题。逐集整理核心概念、代码思路、面试答法与易错边界,并校正响应式、Pinia、diff 和 nextTick 中的常见误解。Vue 响应式底层原理:Proxy 与视图更新(P6)从手动更新 DOM 出发,理解 Proxy 的 get/set 拦截、与 Vue 2 属性拦截的区别,以及对象、数组和原始对象访问的边界,厘清拦截变化与依赖追踪的关系。前端面试 P081:StrictMode 的检查与版本边界JavaScript 函数传参:值传递与对象引用前端面试 P018:手写柯里化与参数个数边界前端面试 P010:普通函数、箭头函数与 this前端面试 P088:Immer 草稿与结构共享前端面试 P006:对象遍历与属性键的选择前端面试 P009:执行上下文、调用栈与词法作用域前端面试 P094:SSE 分片解码与事件边界前端面试 P097:请求并发池、任务启动与失败释放前端面试 P076:Fiber 的协作调度与不可中断边界前端面试 P013:节流、防抖与取消待执行任务前端面试 P033:Object.create、描述符与共享属性前端面试 P057:Vue Router 配置、历史模式与导航前端面试 P030:构造函数与 new 的返回规则前端面试 P077:key、组件身份与状态重置前端面试 P039:同源策略、CORS 与请求是否发出前端面试 P032:instanceof、原型链与自定义判断前端面试 P047:Proxy 与 Vue 2 属性拦截的区别前端面试 P019:函数组合与同步数据管道前端面试 P004:手写深拷贝、循环引用与实现边界前端面试 P059:导航守卫、顺序与权限边界前端面试 P083:函数式更新、队列与纯度前端面试 P014:call、apply、bind 与模拟实现边界前端面试 P012:高阶函数、柯里化与部分应用前端面试 P041:事件委托、目标匹配与 React 事件前端面试 P048:track、trigger、动态依赖与清理前端面试 P066:自定义指令与 DOM 资源清理前端面试 P075:props 只读、状态归属与派生数据前端面试 P062:Pinia 创建、实例范围与使用前端面试 P073:JSX 编译、React 元素与表达式前端面试 P026:Promise 延迟与顺序任务队列前端面试 P016:纯函数、副作用与参数共享前端面试 P094:流式文本的 SSE 与 WebSocket 取舍前端面试 P064:storeToRefs 与 action 解构纠错前端面试 P044:Composition API 与组合式函数前端面试 P051:副作用清理与过期请求前端面试 P021:事件循环、微任务与渲染机会前端面试 P003:原始值、对象共享与赋值行为前端面试 P098:用户偏好存储、TTL 与命名空间前端面试 P056:插槽作用域与 slot props前端面试 P011:闭包、变量绑定与循环回调前端面试 P027:async 错误处理与 return await前端面试 P036:CSP、nonce 与防护边界前端面试 P007:隐式转换、ToPrimitive 与空数组比较前端面试 P035:继承方式对照与寄生组合式继承前端面试 P025:手写 Promise 的最小状态机前端面试 P079:受控表单与非受控输入前端面试 P072:虚拟 DOM 的价值与性能边界前端面试 P005:数组方法、可变性与回调副作用前端面试 P100:Proxy 沙箱为何不是安全边界前端面试 P040:发布订阅、事件总线与退订前端面试 P053:props、组件事件与 v-model 约定前端面试 P008:相等比较、NaN 与 Object.is前端面试 P065:Pinia 最小变更与对象替换的取舍前端面试 P087:Redux 单向数据流与状态边界前端面试 P002:数据类型与 typeof 的陷阱前端面试 P085:useReducer 与 useState 的实际取舍前端面试 P078:React 合成事件与旧事件池纠错前端面试 P025:Promise 解析过程与实现验收前端面试 P043:Vue 渐进式与声明式 UI前端面试 P091:SWR、TanStack Query 与服务端缓存前端面试 P074:函数组件快照与类实例的输出纠错前端面试 P071:UI 是状态映射与渲染纯度前端面试 P054:provide、inject 与跨层级依赖前端面试 P061:何时引入全局状态管理前端面试 P097:请求去重、缓存键与过期策略前端面试 P086:Context 拆分与订阅更新边界前端面试 P024:Promise 并发组合与空输入边界前端面试 P089:Redux middleware、next 与异步返回值前端面试 P022:Promise 状态、链式调用与错误传播前端面试 P084:状态提升、单一来源与范围选择前端面试 P034:class、super 与语法糖的边界前端面试 P058:路由参数、query 与组件 props前端面试 P020:定时器延迟、轮询重叠与清理前端面试 P029:对象原型、prototype 与属性查找前端面试 P095:AI SDK、编排框架与生成 UI 工具前端面试 P092:Signals、细粒度依赖与 React 集成边界前端面试 P046:Vue 响应式陷阱与 ref 自动解包纠错前端面试 P090:Zustand、Jotai 与状态作用域前端面试 P100:微前端 JS 隔离方案与资源生命周期前端面试课程:学习目录与纠错记录前端面试 P023:async、await、异步恢复与并发前端面试 P068:Vue 2 与 Vue 3 的列表 diff 区别前端面试 P015:IIFE、作用域隔离与现代模块前端面试 P063:Pinia 状态、getter 与 action前端面试 P052:Vue 条件、列表与表单指令前端面试 P050:Vue 生命周期、DOM 时机与 SSR前端面试 P060:路由 meta、合并与界面配置前端面试 P045:ref、reactive、computed 与 watch前端面试 P096:令牌无感刷新、重试与安全边界前端面试 P093:MCP 架构与前端集成场景前端面试 P080:组合、children 与自定义 Hook前端面试 P082:useState 更新请求、快照与批处理前端面试 P067:Vue 模板编译与运行时边界前端面试 P031:手写 new 与 Reflect.construct 的区别前端面试 P069:nextTick 等待 DOM 刷新与绘制的区别前端面试 P055:默认插槽、具名插槽与内容接口前端面试 P038:嵌套定时器的最小延迟与调度选择前端面试 P099:倒计时截止时间、漂移与完成事件前端面试 P019:记忆化缓存与递归调用的纠错前端面试 P049:Vue 与 React 的更新范围和批处理前端面试 P004:浅拷贝、深拷贝与 structuredClone前端面试 P042:Vue 面试导学与学习顺序前端面试 P096:共享刷新 Promise 与并发失败恢复前端面试 P070:React 面试导学与复习主线前端面试 P028:异步输出顺序的逐步推演前端面试 P001:var、let、const 与暂时性死区前端面试 P037:内存泄漏的快照与引用链排查前端面试 P017:手写 bind、重复绑定与构造调用Welcome to Henry’s LogA place to keep programming notes, project updates and the things I learn along the way.
Esc to close
All articles

Vue 面试完整笔记:响应式、组件、路由、Pinia 与渲染机制

Showing the original in Chinese (China)

来源:程序员方格《3 小时逆袭 Vue 面试》完整合集。实际包含 28 集:1 集前言与 27 节专题,总时长 3 小时 45 分 23 秒。本笔记按各集课件和代码演示整理,保留逐集来源;技术边界结合 Vue 3、Vue Router 4 与 Pinia 官方文档校正。

全集主线

理解 Vue,可以沿着一条链路展开:声明状态 → 模板读取状态并建立依赖 → 状态变化触发相关副作用 → 调度器合并更新 → 渲染与 patch 更新 DOM → 在适当时机读取 DOM、释放资源。

组件通信、路由与 Pinia 分别解决状态和行为如何跨组件组织、页面如何切换、共享业务状态如何维护。

模块 对应集数 要解决的问题
基础与响应式 P01–P08 为什么这样设计、数据为什么能驱动视图
生命周期与组件 P09–P15 副作用放在哪里、组件如何传递数据与内容
路由 P16–P19 如何导航、传参、拦截导航和组织页面元信息
全局状态 P20–P24 什么时候使用 Pinia、如何保持正确的状态关系
DOM 与渲染机制 P25–P28 指令、模板编译、diff 与异步更新之间的联系

一、基础与响应式

P01 · 前言:从会用 API 到理解机制

课程的学习路径是设计哲学、核心 API、响应式原理和工程应用。复习时可以对每个 API 追问三件事:它解决什么问题、依赖什么机制、有哪些适用边界。

例如,知道 nextTick 的调用方式只是第一步;还要解释为什么 DOM 会延后更新、它等待哪个更新阶段,以及为什么它不能保证请求完成或画面已经显示。

P02 · Vue 的设计哲学与核心理念

  • 渐进式: 可以在已有页面中使用 Vue,也可以配合组件、构建工具、路由和状态管理开发完整应用。按项目需要采用相应能力。
  • 声明式 UI: 用模板描述状态与界面的关系,由框架维护更新。计数器示例中,改变 count 后,插值、条件区域和列表按各自依赖更新。
  • 组件化: 将界面拆成具有明确输入、输出与内容接口的单元,便于复用和维护。
  • 编译与运行时协作: 模板先被编译成渲染函数,运行时结合响应式和渲染器完成更新。

面试答法: Vue 以声明式、组件化的方式组织界面,通过响应式追踪和编译优化减少手动维护 DOM 的工作;“渐进式”描述的是采用方式和生态组合能力。

P03 · Composition API 与逻辑组织

Options API 按 data、methods、computed、生命周期等选项组织代码;Composition API 可以将同一业务功能的状态、派生值、副作用和清理逻辑放在一起,并提取为组合式函数。

课程以倒计时和表单校验为例:当一个组件包含多个功能时,按功能聚合逻辑,更容易看清每个功能的输入、输出和生命周期。

  • setup(props, context) 是组合式 API 的入口之一;普通 setup 返回的绑定可供模板使用。
  • <script setup> 是编译期语法,减少返回绑定等样板代码,其中顶层绑定可直接用于模板。
  • useCounter() 一类组合式函数封装可复用的有状态逻辑;在哪里创建状态,决定多个调用者是否共享状态。
  • 与 React Hooks 可以类比用途,但执行模型不同。Vue 的 setup 通常每个组件实例执行一次,响应式副作用随后按依赖重新运行;不能据此套用 React 的闭包和 Hook 规则。

P04 · 核心语法:ref、reactive、computed、watch

API 作用 主要边界
ref(value) 用 .value 保存一个可追踪、可替换的值 可保存基本类型,也可保存对象;对象默认会进行深层响应式转换
reactive(object) 返回对象的深层响应式代理 用于对象、数组及受支持的集合;不能把数字、字符串直接变为响应式代理
computed(getter) 根据依赖产生派生值,按依赖缓存计算结果 默认只读,getter 应保持纯粹;请求、计时器等副作用不宜放在其中
watch(source, callback) 显式侦听数据源并执行副作用 watch(() => state.count, ...) 保留读取关系;传入 state.count 只是当下的普通值
watchEffect(effect) 立即运行,并自动追踪同步执行期间读到的依赖 异步函数首次 await 之后读取的属性不会由该次同步追踪自动收集
const count = ref(0);
const doubled = computed(() => count.value * 2);
watch(count, (value, previous) => {
  console.log(previous, "→", value);
});

校正: 课程将 ref 简化为“基本类型”,这只是常见使用方式。ref({ count: 0 }) 同样有效,也具有默认的深层响应式能力。默认只读的 computed 应修改其来源状态;需要可写 computed 时,应明确提供 getter 和 setter。

P05 · 响应式陷阱:解构、替换与自动解包

场景 正确理解与处理
const { name } = reactiveUser,其中 name 是字符串 拿到的是当次读取的值;用 toRef(user, "name")、toRefs(user) 或保留 user.name 的访问方式
解构拿到嵌套响应式对象 这个对象代理本身仍可能是响应式的,但不会自动跟随父对象将该属性替换成另一个对象
state.value = newObject,其中 state 是 ref 合法且可触发更新;保存着旧 state.value 的其他变量仍指向旧对象
把保存 reactive 代理的变量改指向新对象 先前建立在旧代理上的引用与依赖不会自动迁移;按业务需要保留代理或使用 ref 容器
Vue 3 中新增、删除代理属性,直接设置数组下标或长度 可以被响应式系统处理,不能沿用 Vue 2 的限制来判断
reactive 普通对象属性中嵌套 ref 通常会自动解包;数组元素、原生集合中的 ref 则需要注意 .value
const user = reactive({ name: "Alice" });
const { name } = toRefs(user);
name.value = "Bob"; // 更新 user.name

const list = reactive([1, 2, 3]);
list[1] = 9;       // Vue 3 可以追踪
list.length = 1;  // Vue 3 可以追踪

课程建议少做整体替换、优先使用数组变更方法,可以作为保持引用关系的习惯;不能推导成“ref 不允许替换”或“Vue 3 数组只能用 splice 更新”。

版本补充: Vue 3.5+ 对同一 <script setup> 中的 defineProps 解构提供编译器支持。这是有条件的编译转换,不能推广成所有普通 JavaScript 解构都会保留响应式。

P06 · Proxy:拦截变化与更新视图之间的区别

Proxy 提供对象操作的拦截入口:读取对应 get,写入对应 set,删除对应 deleteProperty,成员检测对应 has,枚举涉及 ownKeys 等入口。

  • state.count++ 先读后写,会涉及 get 与 set。
  • 只写 get、set,不会自动获得对删除、枚举等操作的自定义处理。
  • 直接修改原始对象会绕过代理;普通 Proxy 也不会自动递归代理所有嵌套对象。
  • Reflect.get/set 可帮助保留常规属性访问语义和 receiver;set 应返回正确的成功结果。

课程在 set 中直接更新计数器 DOM,是为了演示如何把拦截与视图连接起来。真正的 Vue 还需要依赖收集、调度、清理及不同对象类型的处理。

与 Vue 2 的逐属性访问器方案相比,Proxy 更便于覆盖对象结构变化和数组操作。Object.defineProperty 本身能定义新属性、也能定义数组索引访问器;限制应结合 Vue 2 的观察方案讨论。

进一步阅读:P6 专题:Proxy、属性拦截与视图更新。

P07 · 依赖追踪:track、trigger 与 effect

核心关系可以理解为 目标对象 → 属性 → 依赖这个属性的副作用集合。教学模型通常用 WeakMap<object, Map<key, Set<effect>>> 表达。

  1. 执行副作用前,记录当前正在运行的 effect。
  2. effect 读取代理属性时,get 触发 track,把当前 effect 记录到相应依赖集合。
  3. 数据写入并发生相关变化时,trigger 找到受影响的 effect。
  4. 按策略执行或调度这些工作;组件更新通常还会经过合并与去重。

教学实现之外还需要处理: 条件分支切换时清理旧依赖、嵌套 effect 恢复上下文、停止订阅、数组和集合操作,以及避免执行期间增删依赖造成重复运行。当前 Vue 源码的数据结构和优化可能不同,不能把这张关系图当成完整源码。

面试重点是说明“谁读取了哪个属性,数据变化后通知谁”,而不仅是背出 WeakMap。

P08 · Vue 响应式更新与 React 状态更新

维度 Vue React
更新入口 修改 ref 或代理状态,触发相关依赖 用状态 setter 等机制请求更新
依赖关系 渲染副作用读取响应式属性时建立依赖 组件重新执行并形成新的渲染结果,结合组件边界与优化决定后续工作
批处理 调度器合并、去重组件更新任务 现代 React 在支持的入口中自动批处理;更新队列和调度规则需结合版本理解
常见误区 解构普通值后丢失联系、绕过代理、混淆 DOM 更新时机 直接突变状态、闭包拿到旧快照、不稳定引用影响子组件优化

不能将其概括为“Vue 只更新一个 DOM,React 必然重绘整页”。两者都需要结合组件、渲染和提交过程判断实际工作量。React 的 memo、Vue 的依赖边界及双方的编译优化,都可能影响更新范围。

Vue 状态写入后可以立即读到新值,DOM 则通常等待更新队列刷新。nextTick 用于等待相关 DOM 更新完成;React 的 flushSync 是强制同步刷新能力,两者用途和代价不同。

二、生命周期与组件

P09 · 生命周期:初始化、更新与释放资源

阶段 常用钩子 典型用途
挂载前后 onBeforeMount、onMounted 挂载前准备;挂载后访问客户端 DOM、初始化第三方组件
更新前后 onBeforeUpdate、onUpdated 在确有需要时比较旧、新 DOM 或与第三方库同步
卸载前后 onBeforeUnmount、onUnmounted 停止计时器、移除监听、关闭连接和销毁外部实例

普通同步父子组件中,子组件的 mounted 通常先于父组件;父子都参与同一轮更新时,子组件的 updated 先于父组件。异步组件、Suspense 等场景需要另外分析。

避免在 onUpdated 中无条件修改参与渲染的状态,以免形成更新循环。生命周期钩子通常在 setup 阶段同步注册。

补充: 数据请求不必统一放在 onMounted。是否依赖 DOM、是否需要 SSR 和首屏数据,决定请求位置;onMounted 不在服务端渲染阶段执行。

P10 · 副作用清理:谁创建,谁负责释放

课程演示了两个典型问题:依赖变化后重复创建定时器,以及旧请求晚返回、覆盖新结果。

  • watcher 的清理回调在本次监听失效、即将重新执行或停止时运行。
  • 定时器、事件监听和订阅应在创建位置附近安排对应清理。
  • 请求可用 AbortController 取消;不能取消的工作,可用版本号或令牌忽略过期结果,同时保护 loading、error 等状态。
watchEffect((onCleanup) => {
  const ms = intervalMs.value;
  const timer = setInterval(() => {
    ticks.value++;
  }, ms);
  onCleanup(() => clearInterval(timer));
});

课程把清理参数命名为 onInvalidate;参数名可以自行选择,关键是注册正确的清理函数。Vue 3.5+ 也提供 onWatcherCleanup,它要求在当前副作用的同步执行阶段调用,不能等到 await 后再注册。

同步创建并归属于组件的 watcher 通常在组件卸载时停止;在脱离组件作用域的异步回调中创建的 watcher,需要关注手动停止。组合式函数可以利用 onScopeDispose 管理作用域资源。

P11 · 模板指令:条件、列表、属性与表单

  • v-if 控制子树的创建与销毁;v-show 通常保留元素并切换 display。频繁显隐和昂贵初始化之间如何取舍,应结合实际场景。
  • v-for 负责列表渲染。会插入、删除、重排或包含输入状态的列表,应使用稳定业务 ID 作为 key,避免索引变化导致组件状态复用到错误项目。
  • v-bind,简写 :,将表达式绑定到属性、class 或 style 等目标。
  • 表单上的 v-model 根据控件类型处理值和事件,并支持 .trim、.number、.lazy 等修饰符。

Vue 3 同一元素上 v-if 的优先级高于 v-for,此时条件表达式无法按想当然的顺序取得循环变量。优先使用计算属性过滤列表,或用 <template> 分层表达。

key 用于识别同级节点的身份;它不是简单的“渲染越快越好”开关,也不要求在全站范围内唯一。

P12 · 组件通信:props、emit 与 v-model

父组件通过 props 向下传值,子组件通过事件表达“发生了什么”或“希望如何修改”,由父组件决定状态变化。props 是单向数据流,避免子组件直接改写输入。

Vue 3 的默认组件 v-model 可以理解为:

<CustomInput
  :modelValue="text"
  @update:modelValue="text = $event"
/>

子组件接收 modelValue,并通过 emit("update:modelValue", value) 请求更新;命名模型使用相应属性和 update:属性名 事件。Vue 3.4+ 的 defineModel 可以减少这层协议的样板代码。

使用 defineProps、defineEmits 或对应选项明确组件契约。不要把“未声明 emits”理解为所有事件都必然无法触发,但声明有助于校验、类型推导及正确处理事件监听与透传。

补充: props 的只读约束是浅层的。对象内部仍可能被修改,但跨越数据所有权的隐式修改会让维护困难,应优先使用明确的事件或操作接口。

P13 · Provide / Inject:跨层级传递依赖

当某个祖先要向多层后代提供主题、配置或局部共享能力时,可用 provide(key, value) 与 inject(key, defaultValue) 避免逐层传递 props。

  • 从当前组件的祖先链寻找最近的匹配提供者;也可使用应用级提供。
  • 可以提供 ref、reactive 对象或 computed,保留相应响应式联系;注入一个普通值不会自动把它变成响应式。
  • 大型项目可用 Symbol key 避免名称冲突;缺省值可处理未提供的情形。
  • 推荐由提供者维护状态,并提供显式修改函数;必要时用 readonly 限制消费者直接写入。

课程用主题切换和颜色配置说明共享效果。它适合具有层级关系的依赖,不等于所有应用状态都应放进 Provide / Inject。

P14 · 默认插槽与具名插槽

子组件定义“内容放在哪里”,父组件决定“放什么内容”。<slot> 是默认内容入口,内部内容可以作为未传入时的回退内容。

布局组件可以分别开放 header、默认主体和 footer 插槽:

<PageLayout>
  <template #header><h1>文章标题</h1></template>
  <p>主体内容</p>
  <template #footer><small>页脚</small></template>
</PageLayout>

明确每个插槽的用途,避免为不稳定的内部细节开放过多接口。插槽传递的是可渲染内容接口,props 传递的是数据,两者可以组合使用。

P15 · 插槽作用域与 slot props

插槽内容通常在父组件的编译作用域中访问变量,不会因为最终出现在子组件内部,就自动获得子组件的局部数据。

子组件需要把数据交给插槽内容时,应显式传递 slot props:

<!-- 子组件内部 -->
<slot :item="item" :index="index" />

<!-- 父组件使用 -->
<ItemRenderer v-slot="{ item, index }">
  <span>{{ index + 1 }}. {{ item.name }}</span>
</ItemRenderer>

可以把它理解成父组件传入一个渲染函数,子组件在调用它时提供参数。数据归属和内容渲染因此保持明确,常用于表格、列表和可定制组件。

三、Vue Router

P16 · 路由的配置与导航

路由将 URL 与对应组件关联。用 createRouter({ history, routes }) 创建路由器,app.use(router) 注册,<RouterView> 提供匹配组件的渲染位置。

  • <RouterLink> 提供声明式导航;router.push、router.replace 提供编程式导航。
  • push 通常增加历史记录,replace 替换当前历史条目。
  • createWebHashHistory 使用 hash;createWebHistory 使用常规路径,部署时需要服务器正确处理应用路由的回退,否则直接访问深层地址可能 404。
  • 路由表中的 path、name、component 各有作用;复杂应用可按路由拆分、懒加载组件。

P17 · 路径参数、查询参数与组件解耦

路径参数适合资源标识,例如 /users/:id;查询参数适合筛选、排序、分页,例如 /search?q=vue&page=2。可选路径参数也存在,不能把所有 params 都说成必填。

router.push({ name: "user", params: { id: "42" } });
router.push({ path: "/search", query: { q: "vue", page: "2" } });

在组合式 API 中,useRoute() 读取当前路由信息,useRouter() 获取导航能力。传入 path 时,params 会被忽略,应使用命名路由或拼出完整路径。

路由记录的 props: true 可将 params 作为组件 props 传入,减少组件对路由器的直接依赖。查询值和可重复参数可能具有不同形态,使用前要解析、校验。

同一组件可能在参数变化时被复用,不能只依赖 mounted 获取新数据;可侦听目标参数,或使用对应的路由更新钩子。

P18 · 导航守卫:进入、更新与离开

课程介绍全局守卫、路由独享守卫和组件内守卫,分别适用于统一登录检查、某条路由的进入条件、参数更新及未保存内容的离开提示。

普通导航中的关键顺序是:离开组件守卫 → 全局 beforeEach → 复用组件的更新守卫 → 路由 beforeEnter → 解析异步路由组件 → 进入组件守卫 → 全局 beforeResolve → 确认导航 → afterEach → 后续 DOM 更新与进入回调。

  • Vue Router 4 可以返回 false 取消导航,返回路由位置进行重定向;异步守卫可以等待必要条件。
  • 使用旧式 next 参数时,每条执行路径要正确调用一次,避免既重定向又继续放行。
  • beforeRouteEnter 运行时没有可用的组件实例 this;组合式 API 提供 onBeforeRouteUpdate、onBeforeRouteLeave 等能力。
  • afterEach 不负责阻止导航,可用于页面标题、统计等,并注意导航失败的情况。

前端守卫管理导航体验;受保护数据的访问仍需由服务端鉴权。登录重定向应避免循环,并按需保存原目标位置。

P19 · 路由元信息 meta

meta 用于为路由附加结构化配置,例如 requiresAuth、页面标题、面包屑或布局信息。守卫通过 to.meta 读取,组件通过当前 route 读取。

嵌套路由的 route.meta 是匹配记录从父到子的非递归合并;子记录的同名字段可以覆盖父记录。不要把嵌套对象误当成深合并结果。

将字段含义和类型约定清楚,避免每个页面自行发明格式。meta 会进入前端代码,不应放入密码等秘密信息。

四、全局状态与 Pinia

P20 · 什么时候需要全局状态

当同一业务数据被多个相距较远的组件、页面共同使用,而且需要统一操作、调试或持久化策略时,可以考虑集中状态管理。

  • 单组件临时输入、展开状态:优先放在组件内部。
  • 父子间的少量数据:优先用 props 和事件。
  • 某个组件子树的共享依赖:可考虑 Provide / Inject。
  • 跨页面共享的用户、业务流程、购物车等状态:可考虑 Pinia。
  • 可以由 URL 表达的筛选与分页:还要考虑是否应由路由参数承载。

全局状态不是越多越好。避免多处保存同一事实并分别更新;需要派生的数据,优先通过计算表达。SSR 场景应隔离不同请求的状态实例。

P21 · 创建和使用 Pinia Store

Pinia 的入口是 createPinia 和 defineStore,不是 Vuex 的 createStore:

const pinia = createPinia();
app.use(pinia);

const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
    },
  },
});

const store = useCounterStore(pinia);
store.increment();
console.log(store.count);

defineStore 定义获取 store 的函数;在相应 Pinia 实例中调用它,获得共享的 store。store ID 应稳定且唯一。

Option Store 按 state、getters、actions 组织;Setup Store 按组合式 API 编写并返回需要暴露的状态、计算值和操作。组件外使用时,注意获取 store 的时机及 Pinia 实例。

P22 · State、Getters、Actions

  • State: 业务事实和可变数据。通常用 store.count 或 store.$state.count 访问;Pinia 不要求默认经过 store.state.count。
  • Getters: 基于 state 的派生值,类似 computed。纯计算通常放这里,避免重复维护一份可推导的数据。
  • Actions: 组织业务操作,可以同步或异步,内部可以修改状态、调用其他 action,集中处理请求结果与错误。

Pinia 允许直接修改状态,也支持 $patch 将相关修改组织在一起,不需要照搬 Vuex mutation 的协议。复杂业务仍应使用清晰的 action 表达操作意图。

Option Store 中跨 getter 访问可使用普通函数与 this,TypeScript 下必要时标注返回类型。返回函数以接收参数的 getter,也不能简单理解为自动缓存每一组调用参数。

Option Store 提供 $reset;Setup Store 通常需要按自身状态结构实现重置逻辑。

P23 · Store 解构:状态用 storeToRefs,action 可以直接取出

const store = useCounterStore();
const { count } = storeToRefs(store);
const { increment } = store;

increment();
console.log(count.value);

直接解构普通 state 值或 getter 值,拿到的可能是当下快照,无法继续跟随 store 变化。storeToRefs 将响应式 state 与 getters 转为 refs,保留联系,并跳过 action 等非响应式成员。

重要校正: 课程把“解构 action”列为错误,这不符合 Pinia 的行为。Pinia action 绑定了对应 store,可以直接解构调用;不需要用 storeToRefs 处理 action。

保留 store.count 的访问方式同样正确。是否解构取决于组件可读性,不能机械地要求所有访问都通过 storeToRefs。

P24 · 最小可变点原则:按业务语义选择修改粒度

只修改一个字段时,直接更新该字段,通常更容易保留对象引用关系,也能缩小部分依赖的失效范围。多个相关变化可以用 $patch 或业务 action 统一表达。

store.user.name = "Alice";
store.$patch((state) => {
  state.user.age = 30;
  state.user.skills.push("Vue");
});

边界: 整体替换对象并不是一律错误。如果业务含义就是用新快照替换旧实体,可以整体替换,并检查旧引用是否还应继续有效。Object.assign 是合并操作,不会自动删除新对象中已经不存在的旧字段。

修改一个字段也不保证“只有一个组件一定重新渲染”。实际更新范围受读取依赖、组件边界、props 及调度影响,需要用性能记录验证。课程里的 console.time 适合观察某段代码耗时,但不能单独代表 DOM 更新和绘制的总耗时。

五、DOM、编译与渲染

P25 · 自定义指令:复用直接 DOM 操作

自定义指令适合需要直接操作元素的复用逻辑,例如自动聚焦、特定 DOM 交互或第三方库集成。业务内容与组件结构通常更适合用组件、组合式函数表达。

  • 全局注册用 app.directive;局部可用组件的 directives 选项,在 <script setup> 中也可使用符合 vXxx 命名约定的绑定。
  • 钩子提供 el、binding 等信息;binding.value 是传入值,还可读取参数、修饰符和旧值。
  • mounted 初始化,updated 处理变化,unmounted 释放监听和外部实例。
  • 课程的 v-color 示例,本质上是把颜色值同步到元素样式;实际使用时应比较普通 :style 是否已经足够。

指令若用于隐藏权限按钮,只能控制界面表现,不能替代服务端的访问控制。避免在多根组件等场景下假定指令总能找到预期根元素。

P26 · 模板编译:从模板到渲染函数

课程用简化编译器演示三个阶段:

  1. 解析: 识别标签、属性、指令、文本与插值,建立 AST。
  2. 转换与优化: 处理条件、循环、绑定等结构,识别静态与动态部分,为后续更新提供信息。
  3. 代码生成: 生成渲染函数;运行时执行它得到 VNode,再交给渲染器创建或更新 DOM。

Vue 3 的优化还涉及静态内容的提升或缓存、Patch Flags、Block Tree 等,以减少不必要的比较。模板编译、响应式触发和 DOM patch 是协作的不同环节,不能把编译本身说成直接更新页面。

版本校正: 课程示例中的 _c、_v、_s 和 with 等教学代码,不能作为 Vue 3 当前生成代码的固定格式。用正则处理几种标签的示例,也没有覆盖真实模板解析、作用域、指令和代码生成的完整规则。

SFC 项目通常在构建阶段预编译模板,运行时不一定需要携带模板编译器。

P27 · diff:节点身份、复用与移动

diff 的任务是在新旧 VNode 之间确定哪些节点可以复用,哪些属性或子节点要更新,以及哪些 DOM 需要插入、删除或移动。

  • 通常围绕同一父级下的节点对比,不做任意跨层级的树编辑搜索。
  • 节点能否复用,与节点类型和 key 等身份信息有关;相同 key 不代表内容不必更新。
  • key 应使用同级内稳定的业务身份。对会重排且含有状态的列表,用索引可能把输入值或组件状态保留在错误的项目上。
  • “有 key”不自动等于所有场景更快;它首先明确身份和复用语义。

重要版本边界: Vue 2 常见的是双端比较;Vue 3 的带 key 子节点处理还包含头尾同步、key 映射及最长递增子序列等步骤,用于减少部分移动。不能把所有 Vue 版本的列表 diff 统一概括为同一种双端算法,也不能把全过程一概写成严格的 O(n):求最长递增子序列通常涉及 O(n log n) 工作。

课程的简化 patch(oldNode, newNode) 演示了创建、删除与更新的分支,不代表完整的 keyed children 算法。

P28 · nextTick:等待 DOM 更新,而非等待一切异步任务

Vue 通常会批量处理响应式变化带来的组件更新。修改状态后立即读取 DOM,可能仍得到旧内容;await nextTick() 用来等待相关更新队列中的 DOM 更新完成。

message.value = "新内容";
await nextTick();
console.log(messageElement.textContent);

三个边界必须区分:

  1. 状态值: 正常的 ref 或 reactive 写入可以立即读到新值;延后的通常是相关 DOM 更新。
  2. 请求完成: nextTick 不等待 fetch、计时器或任意业务 Promise。应先等待业务工作,再按需要等待 DOM 更新。
  3. 屏幕绘制: DOM 已更新不等于浏览器已经完成布局、绘制或动画。需要逐帧控制时,应另外分析浏览器渲染时机。
loading.value = true;
try {
  const data = await loadData();
  rows.value = data;
} finally {
  loading.value = false;
}
await nextTick(); // 此时再读取列表更新后的 DOM

重要校正: 课程将“发起请求后 nextTick 再显示结果”作为数据加载完成的方案,这会造成误解。业务请求必须通过其自身的 Promise 等机制等待,nextTick 不能替代它。

复习时优先纠正的误解

常见表述 应记住的边界
ref 只能保存基本类型 ref 可以保存对象,对象默认也具有深层响应式能力
Vue 3 新增属性、改数组下标不更新 通过相应响应式代理操作可以追踪;应与 Vue 2 限制区分
整体替换 ref 对象必然失效 替换 .value 合法,保存旧对象的引用不会自动改指向
Proxy 自己会更新视图 还需要依赖管理、调度与实际更新逻辑
Vue 3 所有响应式都靠 Proxy ref 的 .value 也通过访问器参与依赖管理
Provide / Inject 自动把任何值变成响应式 响应式联系取决于所提供的值及访问方式
Pinia 用 createStore、store.state 作为默认入口 使用 createPinia、defineStore,以及 store 字段或 $state
action 解构以后不能调用 Pinia action 已绑定 store,可以直接解构
改一个字段一定只渲染一个组件 要看真实依赖和组件更新路径
所有 Vue 列表 diff 都是 O(n) 双端比较 Vue 2 与 Vue 3 的实现、分支和复杂度需要区分
nextTick 能等待数据加载或保证画面已显示 它等待 Vue 的相关 DOM 更新完成

面试串讲示例

Vue 用声明式模板表达状态与界面的关系。Vue 3 的对象响应式主要通过 Proxy 拦截操作,ref 的 value 也通过访问器参与追踪;渲染副作用读取数据时建立依赖,数据变化后由调度器合并相关更新,再执行渲染与 patch。组件之间用 props、事件和插槽明确数据与内容接口,跨层依赖可用 Provide / Inject,跨页面业务状态可由 Pinia 管理。路由负责 URL、页面匹配与导航流程。副作用要考虑重跑和卸载时清理,DOM 读取则区分状态写入、nextTick 后的 DOM 更新与浏览器绘制。遇到性能问题,应结合版本和实际依赖定位,不机械套用“最小修改一定最快”之类结论。

图谱中的进一步阅读

官方参考

More articles →