Vue.js 语法速查表 - Vue 3 常用 API 与组合式编程大全
这张速查面向使用 Vue 3 组合式 API 与 <script setup> 写产品的开发者。内容从响应式基础(ref 与 reactive 的区别)讲到 computed 与 watch,再到 props/emits/provide-inject 的跨组件通信,接着覆盖路由、Pinia 状态管理和模板语法,最后是用 shallowRef、defineAsyncComponent 做性能优化。每条都是能直接贴进现有 SFC 并立刻看到响应式效果的片段。读完你应能用 ref/reactive 管理本地状态、用 watch 响应变化、打通父子组件的数据往来,并给大列表和重组件做懒加载。
典型使用场景
面向使用 Vue 3 组合式 API 的前端开发者。表内按「响应式基础 / 生命周期 / 组件通信 / 组合式函数 / 路由与状态 / 模板语法 / 性能优化」把日常开发的主干一次铺开:状态层用 ref 处理基本类型、reactive 处理对象,computed 做派生缓存,watch 与 watchEffect 分别对应「明确监听」和「自动收集依赖」;解构 reactive 会丢响应式,所以用 toRefs 保留;可写计算属性、watch 的 immediate 选项覆盖了初始化即取值的场景。组件层靠 defineProps / defineEmits / defineExpose / defineModel 这组编译器宏,以及 provide/inject 处理跨层级数据。往上还有两块提升工程质量的手段:把逻辑抽成 useXxx 组合式函数并在 onUnmounted 里清理副作用;性能上用 shallowRef、markRaw、v-memo、defineAsyncComponent、KeepAlive 与 Suspense 控制渲染与加载成本。
响应式基础 8
const count = ref(0)count.value++const state = reactive({ name: "Vue" })const doubled = computed(() => count.value * 2)const w = computed({ get: () => v.value, set: (x) => (v.value = x) })watch(count, (n, o) => {}, { immediate: true })watchEffect(() => console.log(count.value))const { name } = toRefs(state)生命周期 6
onBeforeMount(() => {})onMounted(() => {})onBeforeUpdate(() => {})onUpdated(() => {})onBeforeUnmount(() => {})onUnmounted(() => {})组件通信 6
defineProps<{ msg: string }>()const emit = defineEmits(["change"])defineExpose({ method() {} })provide("theme", "dark")const theme = inject("theme", "light")const model = defineModel<string>()组合式函数 5
function useCounter(n = 0) { const c = ref(n); return { c, inc: () => c.value++ } }const { c, inc } = useCounter(10)function useMouse() { const x = ref(0); const y = ref(0); return { x, y } }function useEventListener(el, ev, cb) { onUnmounted(() => el.removeEventListener(ev, cb)) }function useFetch(url) { /* return { data, error, loading } */ }路由与状态 7
const route = useRoute()const router = useRouter()router.push("/users")router.replace("/login")const store = useCounterStore()const { count } = storeToRefs(store)store.$patch({ count: 1 })模板语法 8
{{ message }}:href="url"@click="handler"v-model="form.name"v-if / v-else-if / v-elsev-for="item in list" :key="item.id"<Teleport to="body"><slot name="header" />性能优化 6
const list = shallowRef([])markRaw(obj)v-memo="[a, b]"defineAsyncComponent(() => import("./Comp.vue"))<KeepAlive include="UserList"><Suspense>参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
ref(0) | 把基本值变成响应式,脚本中读写需 .value | const count = ref(0) |
count.value | 读取或修改 ref 的值,模板中自动解包 | count.value++ |
reactive({ }) | 让对象每个属性都响应式,可直接属性访问 | const state = reactive({ name: "Vue" }) |
computed(() => ...) | 派生只读值,依赖变化时才重算 | const doubled = computed(() => count.value * 2) |
computed({ get, set }) | 可写计算属性,把赋值转发到源状态 | computed({ get: () => v.value, set: (x) => (v.value = x) }) |
watch(src, cb, opts) | 侦听变化,回调拿到新旧值 | watch(count, (n, o) => {}, { immediate: true }) |
watchEffect(fn) | 自动追踪 fn 内用到的依赖,任一变化即重跑 | watchEffect(() => console.log(count.value)) |
toRefs(obj) | 解构成一个个 ref,解构后仍保持响应式 | const { name } = toRefs(state) |
onBeforeMount | 挂载前触发,此时 DOM 尚未生成 | onBeforeMount(() => {}) |
onMounted | 挂载后触发,DOM 已就绪,常用于发请求 | onMounted(() => {}) |
onBeforeUpdate / onUpdated | 数据变化重渲前 / 重渲染完成后触发 | onUpdated(() => {}) |
onBeforeUnmount / onUnmounted | 卸载前 / 彻底卸载后触发,用于清理定时器 | onUnmounted(() => clearInterval(t)) |
defineProps<{ }>() | 声明 props(类型写法,无需 import) | defineProps<{ msg: string }>() |
defineEmits([ ]) | 声明向父组件发出的事件 | const emit = defineEmits(["change"]) |
defineExpose({ }) | 把方法暴露给父组件的 ref | defineExpose({ method() {} }) |
defineModel<string>() | 组件双向绑定,Vue 3.4+ 提供 | const model = defineModel<string>() |
provide / inject | 向后代注入数据并接收,可带默认值 | inject("theme", "light") |
useXxx() 组合式函数 | 把状态与副作用封装成可复用钩子 | const { c, inc } = useCounter(10) |
useEventListener(el, ev, cb) | 封装事件监听并在卸载时自动移除 | useEventListener(el, ev, cb) |
useFetch(url) | 封装异步请求,返回 data/error/loading | function useFetch(url) {} |
useRoute / useRouter | 获取当前路由信息 / 路由实例 | const router = useRouter() |
router.push / replace | 编程式跳转 / 替换当前路由不留历史 | router.replace("/login") |
useCounterStore() | 获取 Pinia store 实例 | const store = useCounterStore() |
storeToRefs(store) | 解构 store 状态并保持响应式 | const { count } = storeToRefs(store) |
store.$patch({ }) | 批量更新 store 状态 | store.$patch({ count: 1 }) |
{{ message }} | 模板文本插值 | <p>{{ message }}</p> |
:href="url" | 属性绑定(v-bind 缩写) | <a :href="url">go</a> |
@click="handler" | 事件绑定(v-on 缩写) | <button @click="handler"> |
v-model="form.name" | 双向绑定到对象属性 | <input v-model="form.name" /> |
v-if / v-else-if / v-else | 条件渲染 | <div v-if="loading">加载中</div> |
v-for + :key | 列表渲染,必须写 key | <li v-for="item in list" :key="item.id"> |
<Teleport to="body"> | 把内容传送到指定 DOM 节点 | <Teleport to="body"> |
<slot name="header" /> | 具名插槽 | <slot name="header" /> |
shallowRef([]) | 浅响应式,只追踪引用变化,大列表优化 | const list = shallowRef([]) |
markRaw(obj) | 标记对象永不转为响应式,省掉代理开销 | markRaw(obj) |
v-memo="[a, b]" | 模板记忆,依赖未变则跳过更新 | v-memo="[a, b]" |
defineAsyncComponent | 异步组件懒加载 | defineAsyncComponent(() => import("./Comp.vue")) |
<KeepAlive include="X"> | 缓存组件实例状态,避免重复创建 | <KeepAlive include="UserList"> |
<Suspense> | 协调异步组件的加载与显示 | <Suspense> |
易错点与避坑指南
现象模板里能显示的值,在脚本里读出来是 undefined
原因ref 在脚本中不会自动解包,访问时漏了 .value
处置脚本中统一写成 count.value,模板里才可以直接写 count
现象把 reactive 对象解构后属性失去响应式,改数据界面不动
原因解构取的是普通值快照,切断了与源对象的联系
处置改用 toRefs(state) 解构成 ref,或整个对象不拆开、直接 state.name 访问
现象watch 写好了却一次都没触发
原因watch 默认是惰性的,只有源值发生变化才执行
处置需要初始就执行一次时加上 { immediate: true }
现象大列表操作明显掉帧,每次刷新都要重渲染整棵子树
原因深层响应式代理带来的开销在长列表上被放大
处置把大对象换成 shallowRef,仅整体替换时手动触发;模板层用 v-memo 跳过无关子树
现象路由跳转后又被浏览器后退回来了
原因用了 router.push 而不是 router.replace,导航被压进历史栈
处置登录、重定向这类不想留历史的场景改用 router.replace("/login")
现象组件切走了,定时器还在跑,接口一直在发
原因副作用写在 onMounted 里,但清理逻辑缺失或写错钩子
处置把清理放进 onUnmounted,与注册逻辑成对出现;事件监听用 useEventListener 这类封装更省心
现象v-for 渲染的列表出现数据串位或状态错乱
原因列表项缺少 :key,diff 时只能按索引复用节点
处置为每个 v-for 加上稳定唯一的 :key,如 :key="item.id"
排障路径
1确认某个值是否真的是响应式
console.log(count.value);脚本中读不到值就检查是否漏了 .value,或该变量本就不是 ref/reactive。
2解构对象并保留响应式
const { name } = toRefs(state)之后 name 是 ref,脚本里用 name.value;直接解构 state 会丢响应性。
3让侦听器在初始化时先跑一次
watch(count, (n, o) => {}, { immediate: true })immediate 让回调立即以旧值为 undefined 触发一次,适合初始化取数。
4把请求放到 DOM 就绪之后
onMounted(() => { loadData() })SSR 或父级尚未渲染完成时 DOM 不可用,onMounted 是发请求的安全位置。
5确认跳转是否被压进了历史栈
router.replace("/login")替换式跳转不留历史;需要「返回上一页」时才改用 router.push("/users")。
6降低长列表的渲染成本
const list = shallowRef([])只在整体替换数据时赋新数组,内部变动靠重新赋值触发,比深层代理省得多。
7按需加载大组件
defineAsyncComponent(() => import("./Comp.vue"))首次进入时才会请求这个 chunk,首屏体积随之下降。
命令示例
toRefs 解构保持响应性
const state = reactive({ name: "Vue", count: 1 })
// 直接解构会丢失响应性
const { name } = state
// 正确:解构为 ref
const { name: n, count } = toRefs(state)reactive 对象的属性是响应式代理,但解构出来的值只是普通快照,改 state.name 不会更新这个变量。toRefs 把每个属性包成 ref,才能安全地传出子组件或用在 computed 里。
解构 Pinia store 同样要走 storeToRefs
const store = useCounterStore()
const { count } = storeToRefs(store) // 保持响应式
const { inc, reset } = store // action 直接解构没问题action 是普通函数,取出来不影响调用;但状态是 ref,取出来就变成快照。混用时把状态走 storeToRefs、action 直接解构,是 Vue 3 项目里最常见的正确写法。
watch 的 immediate 让首帧就执行一次
watch(
() => props.userId,
(n, o) => {
load(n)
},
{ immediate: true },
)watch 默认只在依赖「变化后」触发,组件首次挂载时不会跑。用 immediate: true 让初始化逻辑和后续变化走同一条路径,避免把请求逻辑重复写两遍。
把副作用清理放进 onBeforeUnmount
let timer
onMounted(() => {
timer = setInterval(poll, 5000)
})
onBeforeUnmount(() => {
clearInterval(timer)
})组件卸载后定时器仍在跑,会持续触发 setState 并报「更新已卸载组件」警告。onBeforeUnmount 里统一清理定时器与事件监听,是组件复用的前提。
大列表用 shallowRef 避免深层代理
const rows = shallowRef([])
rows.value = await fetchRows() // 整体替换触发更新
// rows.value[0].name = "x" 不会触发更新,需要 rows.value = [...rows.value]shallowRef 只代理到第一层,几千条数据的表格不会因为深层 Proxy 明显掉帧。代价是数组内部字段的修改不会自动更新视图,改完必须整体替换引用。
常见坑与注意事项
- 直接解构 reactive 对象或 Pinia store 会丢失响应性:脚本里再改原对象页面纹丝不动,排查时极易误判为「响应式失效」,实际是拿到的快照。
- 在 computed 里写有副作用的逻辑(发请求、改 DOM、累加变量)会因为求值时机不确定而重复执行;副作用请放进 watch 或 watchEffect。
- v-for 缺少 :key 会让 diff 复用错节点,表现为输入框内容串行、删除一行后状态错位;key 必须用稳定唯一 id,不要用数组下标。
- 组件卸载后仍在 await 异步请求,返回时组件已销毁会造成内存泄漏与状态告警;onBeforeUnmount 里 abort 请求或用 onUnmounted 做清理。
- 在同一个对象上同时用 ref 和 reactive 管理同一份状态会产生两份互不同步的代理,写入一处另一处不更新;一个状态只选一种容器。
- store.$patch 批量更新是浅层合并,传对象会整体替换同名字段而非深合并,嵌套对象请拆成多次调用或直接改具体属性,避免误删同级的其他键。
提示
- ref 用于基本类型,reactive 用于对象;ref 在脚本中需 .value 访问,模板中自动解包。
- computed 有缓存只在依赖变化时重算;watch 默认懒执行,加 immediate: true 可立即触发一次。
- defineProps、defineEmits、defineExpose、defineModel 是编译器宏,无需 import 即可使用。
- 解构 reactive 对象会丢失响应式,用 toRefs 包装后再解构可保持响应式。
- 大列表用 shallowRef + v-memo,路由组件用 defineAsyncComponent 懒加载可提升性能。
常见问题
ref 和 reactive 该怎么选?
ref 用于基本类型和可能整体替换的对象,读写都要经 .value(模板中会自动解包);reactive 用于一组需要整体响应式的属性,可以直接属性访问,脚本和模板里都写 state.name。实践中建议组件状态统一用 ref,因为 ref 能整体替换、不容易漏掉依赖,而 reactive 在解构时会丢失响应性。
为什么 setup 里解构 props 会导致响应性丢失?
因为解构操作把响应式对象上的属性复制成了普通值,副本与原对象不再有联动。要保留响应性就直接用 props.xxx,或者用 toRefs 把 props 转成一组 ref,解构后每个 ref 仍指向原属性。const { msg } = toRefs(props) 之后再解构 msg.value 就能正确响应变化。
computed 和 watch 该怎么选?
computed 用于「由现有状态推导出的值」,有缓存、依赖不变就不重算,适合总数、格式化结果这类派生数据。watch 用于「状态变化时要执行副作用」,比如请求接口、写 localStorage、操作 DOM,默认是惰性的,需要 immediate 或 deep 才能改变时机或深度。能用 computed 解决的不要用 watch,前者更简洁也更高效。
provide 和 inject 适合在什么场景使用?
适合跨多层组件传递同一份数据,避免逐层透传 props。provide("theme", "dark") 在父组件提供,inject("theme", "light") 在后代接收并设默认值。它不限于父子关系,任意后代都能取到。表单、主题、i18n 这类全局关注点适合用它;但如果数据只在一两层内传递,props 更显式也更好追踪。
大列表渲染卡顿怎么优化?
两个方向。一是减少不必要的响应式:几千行的列表用 shallowRef 配合 markRaw 跳过深层代理,省下大量开销。二是限制渲染范围:异步组件懒加载 defineAsyncComponent(() => import("./Comp.vue")),以及用 v-memo 缓存未变化的模板片段。真实项目里通常瓶颈在数据量,先考虑分页或虚拟滚动。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。