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 响应变化、打通父子组件的数据往来,并给大列表和重组件做懒加载。

编程语言·共 46 条命令·最后更新 2026-07-21

典型使用场景

面向使用 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)
把基本数字 0 变成响应式,脚本里读写都经 count.value
count.value++
改 ref 值(脚本中需 .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 })
侦听 ref 变化,(n, o) 是新旧值;immediate 让初始立刻触发一次
watchEffect(() => console.log(count.value))
自动追踪回调里用到的依赖,任一变化即重跑
const { name } = toRefs(state)
把响应式对象解构成一个个 ref,保留响应性

生命周期 6

onBeforeMount(() => {})
挂载前,此时 DOM 还未生成
onMounted(() => {})
挂载后,DOM 已就绪,常用于发请求或操作 DOM
onBeforeUpdate(() => {})
响应式数据变化导致重渲前触发
onUpdated(() => {})
重渲染完成之后触发
onBeforeUnmount(() => {})
卸载前调用,适合清理定时器与事件监听
onUnmounted(() => {})
组件彻底卸载后触发

组件通信 6

defineProps<{ msg: string }>()
声明 props(类型方式)
const emit = defineEmits(["change"])
声明向父组件发送的事件
defineExpose({ method() {} })
暴露方法给父组件 ref
provide("theme", "dark")
向后代组件注入数据
const theme = inject("theme", "light")
接收注入并设置默认值
const model = defineModel<string>()
双向绑定(Vue 3.4+)

组合式函数 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()
获取 Pinia store
const { count } = storeToRefs(store)
解构 store 状态保持响应式
store.$patch({ count: 1 })
批量更新 store 状态

模板语法 8

{{ message }}
文本插值
:href="url"
属性绑定(v-bind 缩写)
@click="handler"
事件绑定(v-on 缩写)
v-model="form.name"
双向绑定
v-if / v-else-if / v-else
条件渲染
v-for="item in list" :key="item.id"
列表渲染(必须加 key)
<Teleport to="body">
将内容传送到指定 DOM 节点
<slot name="header" />
具名插槽

性能优化 6

const list = shallowRef([])
浅响应式(大对象/大列表优化)
markRaw(obj)
标记对象永不响应式
v-memo="[a, b]"
模板记忆(Vue 3.2+)
defineAsyncComponent(() => import("./Comp.vue"))
异步组件懒加载
<KeepAlive include="UserList">
缓存组件实例状态
<Suspense>
协调异步组件加载

参数矩阵

参数作用示例
ref(0)把基本值变成响应式,脚本中读写需 .valueconst 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({ })把方法暴露给父组件的 refdefineExpose({ 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/loadingfunction 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. 1确认某个值是否真的是响应式

    console.log(count.value);

    脚本中读不到值就检查是否漏了 .value,或该变量本就不是 ref/reactive。

  2. 2解构对象并保留响应式

    const { name } = toRefs(state)

    之后 name 是 ref,脚本里用 name.value;直接解构 state 会丢响应性。

  3. 3让侦听器在初始化时先跑一次

    watch(count, (n, o) => {}, { immediate: true })

    immediate 让回调立即以旧值为 undefined 触发一次,适合初始化取数。

  4. 4把请求放到 DOM 就绪之后

    onMounted(() => { loadData() })

    SSR 或父级尚未渲染完成时 DOM 不可用,onMounted 是发请求的安全位置。

  5. 5确认跳转是否被压进了历史栈

    router.replace("/login")

    替换式跳转不留历史;需要「返回上一页」时才改用 router.push("/users")。

  6. 6降低长列表的渲染成本

    const list = shallowRef([])

    只在整体替换数据时赋新数组,内部变动靠重新赋值触发,比深层代理省得多。

  7. 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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们