Vue 基础与现代实践:响应式、模板、组件与生命周期
本文目录
原始笔记日期为 2018-08-01;本文于 2026-10-20 结合 Vue 3 官方指南重新整理。示例采用 Vue 3 Composition API,并单独标记维护 Vue 2 项目时需要注意的差异。
先确认:Vue 2 和 Vue 3 的概念不要混写
Vue 用响应式状态描述界面:状态变化后,框架更新受影响的视图。Vue 2 主要通过 Object.defineProperty 建立响应式属性;Vue 3 的响应式对象主要基于 Proxy,而 ref 用于包装独立值。不要把“数据双向绑定”当成所有数据流的总规则:组件通常仍遵循父级向下传 props、子级通过事件表达更新意图的单向数据流。
开始维护前先检查 vue、构建插件、路由、状态管理和组件库的实际版本。Vue 2 与 Vue 3 有相似概念,但部分生命周期名称、事件 API、v-model 约定和生态插件并不相同。
常用模板指令与边界
| 指令 | 用途 | 注意事项 |
|---|---|---|
v-bind / : | 绑定属性或组件 props | URL、样式和用户输入仍需按上下文校验 |
v-on / @ | 绑定事件;可用 .stop、.prevent、.once 等修饰符 | 修饰符改变事件行为,不替代业务权限检查 |
v-model | 在表单控件与状态间同步 | Vue 2、Vue 3 的组件模型默认 prop/event 名称不同 |
v-if、v-else-if、v-else | 条件创建或销毁 DOM | 频繁切换且需保留节点时评估 v-show |
v-show | 保留 DOM,只切换显示状态 | 首次仍会创建节点;不适合所有隐藏内容场景 |
v-for | 渲染列表 | 为有状态列表项提供稳定且唯一的 :key;避免在同一节点混用 v-if 和 v-for |
v-once | 只渲染一次静态子树 | 仅用于之后确实无需响应式更新的内容 |
v-text | 把值作为文本写入元素 | 插值语法通常更易读 |
v-html | 把字符串解释为原始 HTML | 不要直接渲染用户输入或不可信内容,避免 XSS |
<script setup>
import { computed, ref } from 'vue';
const keyword = ref('');
const items = ref([
{ id: 'props', title: '组件边界' },
{ id: 'state', title: '状态归属' },
]);
const filteredItems = computed(() => {
const query = keyword.value.trim().toLocaleLowerCase();
return items.value.filter((item) =>
item.title.toLocaleLowerCase().includes(query),
);
});
</script>
<template>
<label>
搜索
<input v-model="keyword" type="search" />
</label>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.title }}
</li>
</ul>
</template>
上例用 computed 表达派生列表,并使用稳定业务 ID 作 key。避免把数组下标当成会重排、插入或删除列表的 key,否则组件内部状态可能跟着错误项目移动。
组件通信:先画清状态所有权
| 场景 | 常见方式 | 约束 |
|---|---|---|
| 父级向子级传数据 | props | 子组件不要直接改父级传入对象;需要更新时向外发事件 |
| 子级表达用户意图 | emit / defineEmits | 事件名与参数应形成稳定、可测试的组件契约 |
| 跨多层级提供依赖 | provide/inject | 适合明确的上下文服务,不要变成隐藏的全局可变状态 |
| 跨页面共享业务状态 | Pinia 等 store | 明确状态所有者、变更入口和 SSR 生命周期 |
| 父子间命令式调用 | 模板 ref / expose | 仅在声明式 props/events 不合适时使用,避免深层实例耦合 |
旧笔记中的 $bus 不是 Vue 内置的全局通信服务。Vue 2 项目可能自行用实例或第三方库实现事件总线;Vue 3 移除了组件实例上的 $on、$off、$once,旧模式不能原样迁移。需要广播的业务状态通常更适合显式 store;独立的一次性事件则要有监听清理和所有权设计。
Vue 2 的 $listeners 也不应直接复制到 Vue 3。Vue 3 将未声明的监听器并入 fallthrough attributes;迁移组件库时检查 emits 声明、$attrs 行为和根节点转发。
生命周期、请求与清理
| 目的 | Vue 2 Options API | Vue 3 Options API / Composition API |
|---|---|---|
| 创建后初始化状态 | created | created;Composition API 的 setup 在 Options 钩子前运行 |
| DOM 挂载后接入浏览器能力 | mounted | mounted / onMounted |
| 响应式更新后处理 DOM | updated | updated / onUpdated |
| 卸载前取消订阅和定时器 | beforeDestroy | beforeUnmount / onBeforeUnmount |
| 卸载后清理完成 | destroyed | unmounted / onUnmounted |
事件监听器、计时器、观察器、WebSocket 和长任务都要有对应的释放路径。浏览器 API 应在客户端挂载阶段接入,组件离开时清理;不要在模块顶层或 SSR 期间直接访问 window、document。异步请求还要处理取消或忽略旧响应,避免用户快速切换页面后旧数据覆盖新结果。
computed 与 watch 各司其职
| 工具 | 适合场景 | 常见错误 |
|---|---|---|
computed | 从现有响应式数据推导可读的派生值;基于依赖缓存 | 在 getter 中发请求、改其他状态或操作 DOM |
watch | 某个明确数据变化后执行副作用,如请求、持久化或同步外部系统 | 忘记处理卸载、快速变化、旧请求覆盖和深层监听成本 |
watchEffect | 自动收集同步执行期间读取的响应式依赖并运行副作用 | 依赖不明显时难以审查;异步代码第一次 await 后读取的值不会作为初始依赖收集 |
若显示值只由当前状态计算出来,优先使用 computed,不要复制成第二份 state。若副作用由点击触发,直接写在事件处理函数里,不要为了调用它而额外建立 watch。
动态组件、缓存与懒加载
动态组件可根据状态切换组件实现;<KeepAlive> 缓存符合规则的组件实例,离开活动视图时不会按普通卸载方式销毁。它不是接口缓存、权限控制或持久化存储,使用前要确认缓存范围、名称匹配、内存占用和失效时机。
路由页面可以使用动态导入按需加载:
const UserPage = () => import('./views/UserPage.vue');
页面是否拆包以及错误如何展示由构建工具和路由集成决定;实际效果应通过生产构建产物与路由加载失败状态验证,而不是只看源码是否出现 import()。
样式与安全边界
<style scoped> 借助编译后的属性选择器缩小普通选择器的影响范围;它不是 Shadow DOM,也不是安全或权限隔离。深层选择器、全局样式、Teleport 和第三方 DOM 都可能跨越预期边界,公共样式仍应审慎命名和测试。
v-html 将内容解释为 HTML,Vue 不会替应用清理其安全性。对外部或用户生成的富文本,应先由可信、严格配置的 HTML sanitizer 按标签、属性、URL 协议白名单清理;不能用正则替代成熟净化器,也不要把 Vue 模板字符串当成可动态执行的内容。
迁移与复习检查
- 锁文件和实际依赖优先于旧教程中的“Vue 最新版”表述。
- Vue 2 响应式限制、Vue 3
ref/reactive、组件事件声明和生命周期名称分开记。 - 列表使用稳定 key;派生展示值用 computed;外部副作用明确启动与清理时机。
- 路由守卫、组件状态和 CSS scoped 都不能代替服务端资源授权。
- 浏览器 API、单例 store 和随机/时间值在 SSR 中要避免跨请求共享或 hydration 不一致。
- 用组件测试验证输入输出契约,用端到端测试检查导航、表单、公共资源和真实错误恢复。
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。