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>>> 表达。
- 执行副作用前,记录当前正在运行的 effect。
- effect 读取代理属性时,
get触发track,把当前 effect 记录到相应依赖集合。 - 数据写入并发生相关变化时,
trigger找到受影响的 effect。 - 按策略执行或调度这些工作;组件更新通常还会经过合并与去重。
教学实现之外还需要处理: 条件分支切换时清理旧依赖、嵌套 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 · 模板编译:从模板到渲染函数
课程用简化编译器演示三个阶段:
- 解析: 识别标签、属性、指令、文本与插值,建立 AST。
- 转换与优化: 处理条件、循环、绑定等结构,识别静态与动态部分,为后续更新提供信息。
- 代码生成: 生成渲染函数;运行时执行它得到 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);
三个边界必须区分:
- 状态值: 正常的 ref 或 reactive 写入可以立即读到新值;延后的通常是相关 DOM 更新。
- 请求完成: nextTick 不等待 fetch、计时器或任意业务 Promise。应先等待业务工作,再按需要等待 DOM 更新。
- 屏幕绘制: 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 更新与浏览器绘制。遇到性能问题,应结合版本和实际依赖定位,不机械套用“最小修改一定最快”之类结论。
图谱中的进一步阅读
- P6 专题:Proxy 与视图更新
- track、trigger、动态依赖与清理
- 响应式陷阱与 ref 自动解包
- 生命周期、DOM 时机与 SSR
- 插槽作用域与 slot props
- Vue Router 配置与导航
- Pinia 创建、实例范围与使用
- Pinia 最小变更与对象替换的取舍
- 模板编译与运行时边界
- Vue 2 与 Vue 3 的列表 diff 区别
- nextTick、DOM 刷新与绘制