这本字典怎么用
按用途分六组:响应式 API、模板语法、组件通信、生命周期、组合式函数、报错速查。每条四件事:签名、一句话语义(写”什么时候用、和谁的区别”)、最小示例、常见坑。字典是查的不是读的——先搜文末速查表定位,再跳回条目。
概念背景见Vue 3 语法要点,上手过程见环境搭建与第一个应用。条目默认运行在 script setup 的单文件组件里。
一、响应式 API
组内共性:响应性靠代理与读写拦截实现,任何”绕开包装直接取值”的写法都会断线。
ref
把任意值包成响应式引用,script 里读写走 .value,模板里自动解包。默认首选。
const count = ref(0)
count.value++ // 有 .value 才有追踪
const list = ref([]) // ref 深层:list.value[0].done = true 同样触发更新
坑:script 里忘写 .value 不报错——count = 1 只是换了局部绑定,界面不动,见报错速查第一条。
reactive
只对对象生效的响应式代理,字段访问不用 .value。适合聚合且不整体替换的表单类状态。
const form = reactive({ name: 'ada', page: 1 })
form.page = 2 // 直接改字段,界面更新
坑:解构、整体替换、按值传参都丢代理;只盯嵌套属性时 watch 还要 deep。与 ref 的完整对比见语法要点篇。
shallowRef
只追踪 .value 本身替换、不深层代理的 ref。大列表、第三方实例(地图、图表对象)放里面避免深层开销。
const points = shallowRef([])
points.value = fetchAll() // 替换整个数组才触发更新
points.value.push(1) // 嵌套变化不触发——这正是用它换来的省
坑:当成普通 ref 用(改嵌套属性)界面不更新,还以为撞了 bug。
computed
派生值,依赖自动收集、结果缓存。依赖不变不重算。
const remaining = computed(() => todos.value.filter(t => !t.done).length)
// 组件间共享:只读 computed 传下去,写操作留给声明处
坑:在 computed 里发请求、改状态——派生链里掺副作用,触发时机不可控,副作用归 watch/watchEffect。
watch
显式声明”盯谁”,默认惰性执行,回调拿到新旧值。
watch(query, (next, prev) => { search(next) }, { immediate: false })
watch(() => form.value.name, cb) // ref 对象要盯嵌套属性:传 getter
watch(list, cb, { deep: true }) // 或整树监听
坑:ref 包对象默认浅侦听,改嵌套属性不触发;immediate: true 会把”首次执行”也接上,别和 watchEffect 混用两套心智。
watchEffect
立即执行一次,执行中读到的响应式数据自动成为依赖。适合把状态同步到 Vue 之外。
watchEffect(() => { document.title = `剩 ${remaining.value} 件` })
坑:回调用到的响应式数据全算依赖——条件分支没走到的读不到,依赖就”少收”了;拿不到旧值,需要新旧值对比就回 watch。
二、模板语法
组内共性:指令(directive)是模板里的动词,v- 前缀;缩写 : 与 @ 等价全写。
{{ }} 插值与 v-once
文本插值,跟着响应式数据变;v-once 让节点只渲染一次。
<p>{{ msg }}(提交于 {{ time }})</p>
<p v-once>版权 {{ year }}</p> <!-- 静态内容,之后不再跟着变 -->
坑:插值输出纯文本,要注入 HTML 得用 v-html——等于放行 XSS,只能用于可信内容。
v-bind(缩写 :)
把表达式绑到属性或组件 prop 上。
<a :href="url" :class="{ active: isActive }">链接</a>
<!-- 对象语法同时管多个 class;false 的键不渲染 -->
坑::class 与静态 class 可并存,不必二选一;绑到 prop 的值是表达式不是字符串,忘写 : 传的是字面量。
v-on(缩写 @)
绑定事件,支持修饰符(modifier)串起来。
<form @submit.prevent="onSubmit"> <!-- .prevent 等价 event.preventDefault() -->
<input @keyup.enter="add" />
坑:修饰符顺序有意义,@click.stop.prevent 与 @click.prevent.stop 行为不同;组件上监听的是 emits 声明的自定义事件,不是原生 DOM 事件。
v-if 与 v-show
v-if 真正增删节点,v-show 只切 display。切换频繁用 v-show,条件很少变用 v-if。
<Detail v-if="selected" /> <!-- 不满足时组件销毁,生命周期会走卸载 -->
<span v-show="remaining">还有剩</span>
坑:v-if 与 v-for 同标签时 v-if 优先级更高,访问不到 v-for 的迭代变量——拆成 template 包一层。
v-for
列表渲染,必须绑 key。
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
坑:用数组下标当 key,头部插入或删除时节点与数据错位,输入框类的局部状态会串行;数据没有 id 就造一个稳定 id。
v-model(表单)
表单控件双向绑定的语法糖::value + @input(复选框是 :checked + @change)。
<input v-model="text" />
<!-- 等价 -->
<input :value="text" @input="text = $event.target.value" />
坑:.lazy 改为 change 事件触发(失焦才同步)、.number 转数字、.trim 去首尾空格,三个修饰符按需叠加;原生组件与自定义组件的 v-model 不是一回事,见 defineModel。
三、组件通信
组内共性:数据从上往下(props),事件从下往上(emits),插槽把渲染内容的决定权交回调用方。
defineProps
声明组件的输入契约,编译器宏,无需 import。
<script setup>
defineProps({ item: { type: Object, required: true }, compact: Boolean })
</script>
<template><li :class="{ compact }">{{ item.text }}</li></template>
坑:props 只读。给 props.item.done 赋值可能”碰巧”改到父组件的对象(引用共享),父组件下一次渲染就覆盖回去,还伴随警告。
defineEmits
声明组件会抛出的事件,子组件通知父组件”该你动手了”。
<script setup>
const emit = defineEmits(['remove', 'edit'])
</script>
<template><button @click="emit('remove', item.id)">删除</button></template>
坑:事件名没声明也能抛,但父组件模板里监听的名字大小写会被浏览器统一转小写(DOM 模板场景),约定用 kebab-case 并在 emits 里列全,别靠记忆。
defineModel(3.4+)
让自定义组件直接吃 v-model 的宏,等价于 modelValue prop + update:modelValue 事件。
<script setup>
const model = defineModel() // 支持多个:defineModel('title')
</script>
<template><input v-model="model" /></template>
坑:.value 赋值触发的是向父组件”提案”,父组件不回绑数据就不会变——双向的另一半永远在父组件手里。
provide / inject
跨层级注入,跳过中间组件逐层传 props。
// 祖先
provide('theme', themeRef) // 传 ref 而非值,保持响应性
// 任意后代
const theme = inject('theme', ref('light')) // 第二参给默认值
坑:注入的数据来源在组件树里不可见,重构时容易变隐式全局;只用于主题、文案、当前用户这类真·全局语义,业务数据老老实实走 props。
slot / 具名插槽 / 作用域插槽
内容分发:默认插槽放主体,具名插槽分区,作用域插槽把子组件数据递给调用方的模板。
<!-- Card.vue -->
<header><slot name="title" /></header>
<main><slot /></main>
<!-- List.vue:item 递出去 -->
<li v-for="t in todos"><slot :item="t">{{ t.text }}</slot></li>
<!-- 调用方 -->
<List v-slot="{ item }"><b :class="{ done: item.done }">{{ item.text }}</b></List>
坑:作用域插槽解构出的 item 是快照语义的错觉——它仍是响应式代理,但别在里面再改它,改数据的路径应该走事件。
四、生命周期
组内共性:script setup 本身覆盖了 beforeCreate/created;常用的只有挂载后的 onMounted 与卸载时的 onUnmounted。
onMounted
DOM 就绪后执行:发首个请求、初始化图表、聚焦输入框。
onMounted(() => { chartRef.value?.init(); fetchList() })
坑:在 setup 顶层直接取 ref 拿到 null——此时还没挂载;也不要在 onMounted 里改一大片状态导致”首屏闪一下”,初始化数据尽量在声明处就绪。
onUnmounted
组件卸载后执行,清理定时器、事件监听、订阅。
const timer = setInterval(poll, 5000)
onUnmounted(() => clearInterval(timer)) // 注册与清理成对出现
坑: setInterval、addEventListener、WebSocket 忘了清理,组件销毁后回调还在引用旧状态,报错或泄漏都在远处炸。
onUpdated 与其余钩子
更新后执行,低频使用;想对数据变化做反应,watch 通常是更准的工具。
onUpdated(() => { saveScrollPos() }) // 任何响应式更新都会走这里,容易放大成循环
坑:onUpdated 里再改状态可能造成”更新触发更新”,边界失控;调试用它看”渲染了什么”可以,业务逻辑别压在上面。
五、组合式函数
组内共性:use 前缀命名、返回 ref/函数、每次调用独立一份状态——逻辑复用的单位。
useXxx 组合式函数
把”状态 + 操作它的函数”打包导出,跨组件复用逻辑而不共享状态。
// useCounter.js
export function useCounter(initial = 0) {
const count = ref(initial)
const inc = () => count.value++
return { count, inc } // 返回 ref,响应性才带得出去
}
// 两个组件分别调用,各拿各的 count
const { count, inc } = useCounter(10)
坑:返回解构后的普通值(return count.value)响应性当场死亡;模块顶层直接写状态则是”全局单例”,所有调用方共享,和组件级复用是两回事,要共享就去用 Pinia。
六、常见报错速查
组内共性:八成不是”Vue 的 bug”,而是踩了两类经典反模式——绕开响应式包装直接取值,或绕开单向数据流直接改别人的数据。先对症状,再回上文找对应条目。
| 症状 | 真实原因 | 修法 |
|---|---|---|
| 改了值界面没反应 | script 里忘写 .value(count = 1 只是换局部绑定) | 改 count.value++;DevTools 面板先确认数据变没变 |
| reactive 解构后不更新 | 解构拿到的是普通值,代理丢了 | toRefs(form) 再解构,或改用 ref 声明 |
| watch 监听对象内部变化不触发 | ref 包对象默认浅侦听 | 传 getter 盯具体属性,或 { deep: true } |
| 列表增删后输入框内容串行 | v-for 用下标当 key | :key 绑稳定业务 id |
| 改 props 收到警告 | 单向数据流:props 属于父组件 | emit 事件让父组件改,或 defineModel |
| 组件销毁后还在发请求/报错 | 副作用没在 onUnmounted 清理 | 定时器、监听、订阅与注册成对出现 |
| v-if 里用 v-for 的变量报 undefined | 同标签时 v-if 优先级更高 | 套 template 分层写 |
| 父组件 v-model 传了但不生效 | 子组件没声明 modelValue/update:modelValue | defineModel()(3.4+)或手写 prop + emit |
速查表
| 条目 | 一句话用途 | 有什么坑 |
|---|---|---|
| ref | 任意值的响应式包装,默认首选 | 忘 .value 静默失败 |
| reactive | 对象代理,免 .value | 解构/替换即失联 |
| shallowRef | 只盯整体替换的省开销 ref | 嵌套改不触发 |
| computed | 带缓存的派生值 | 里面别掺副作用 |
| watch | 显式侦听,拿新旧值 | ref 对象默认浅侦听 |
| watchEffect | 立即执行、自动收依赖 | 拿不到旧值,分支影响收集 |
| {{ }} / v-once | 文本插值 / 一次性渲染 | v-html 放行 XSS |
| v-bind(:) | 绑属性与 prop | 忘冒号传成字符串 |
| v-on(@) | 绑事件与修饰符 | 修饰符顺序有语义 |
| v-if / v-show | 增删节点 / 切 display | 与 v-for 同标签优先级坑 |
| v-for | 列表渲染 | 下标当 key 会串位 |
| v-model | 表单双向绑定(语法糖) | 原生与组件版不是一回事 |
| defineProps | 声明输入契约 | 只读,改了警告 |
| defineEmits | 声明输出事件 | 不声明也能抛,靠约定兜底 |
| defineModel | 组件吃 v-model(3.4+) | 另一半在父组件手里 |
| provide/inject | 跨层级注入 | 隐式全局,慎用于业务数据 |
| slot 三兄弟 | 内容分发 | 作用域插槽里别改数据 |
| onMounted | DOM 就绪初始化 | 顶层取 ref 是 null |
| onUnmounted | 卸载清理 | 忘清理在远处炸 |
| onUpdated | 更新后回调 | 可能循环更新,低频用 |
| useXxx | 逻辑复用打包 | 返回普通值响应性死亡 |
边界与延伸
本字典只收基础层:Pinia、Router、SSR(Nuxt)各有专门文档与生态,不在这里铺开;watch 的 flush 时机、effectScope 等进阶话题同样以官方 API 索引为准。React 背景的读者,watch/watchEffect 与 useEffect 的执行模型差异值得对照《React 渲染与重渲染》想一遍——什么时候跑、跑几遍,两套框架答案不同。
版本核实(撰写时检索,2025-03):Vue 当前稳定线为 3.5.x;defineModel 自 3.4 稳定;v-model 参数、作用域插槽均为 Vue 3 稳定特性;3.6(Vapor Mode)仍在实验、未发布,本字典未涉及。具体以官方文档为准。