Vue Composition API 完全指南:setup、ref、reactive、computed、watch、生命周期与自定义 Composable

Vue 3 Composition API 深度实践:setup 语法糖、ref/reactive 响应式原理、computed 计算属性、watch/watchEffect 侦听器、生命周期钩子、provide/inject 依赖注入、defineProps/defineEmits 组件通信,以及自定义 Composable 的开发与复用模式。

Composition API 是 Vue 3 最重要的架构变革。它将组件的逻辑按"关注点"组织(而非按 Options API 的 data/computed/methods 分散),使复杂组件更易维护、逻辑更易复用。本文是 Vue Composition API 的完整参考手册。


一、<script setup>:Composition API 的语法糖

<script setup> 是 Vue 3.2+ 推荐的单文件组件写法,编译时自动将组件的逻辑转化为 setup() 函数。

<script setup>
// 所有变量和函数自动暴露给模板
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

等价于:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    return { count, increment }
  },
}
</script>

二、ref 与 reactive:响应式基础

2.1 ref:值的响应式包装

import { ref } from 'vue'

const count = ref(0)          // Ref<number>
const name = ref('Alice')     // Ref<string>
const user = ref({ age: 25 }) // Ref<{ age: number }>

// 读取/修改都需要 .value
console.log(count.value)      // 0
count.value++                 // 触发更新
console.log(count.value)      // 1

// 模板中自动解包,无需 .value
// <template><p>{{ count }}</p></template>

2.2 reactive:对象的深度响应式

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'Alice', age: 25 },
  items: [],
})

state.count++              // 直接修改,无需 .value
state.user.name = 'Bob'    // 深度响应式
state.items.push('apple')  // 数组方法正常工作

2.3 ref vs reactive 选型表

场景推荐原因
基本类型refreactive 不支持基本类型
对象字面量reactive 或 refreactive 代码更简洁
需要解构赋值refreactive 解构会丢失响应性
作为函数返回值ref避免 reactive 的展开陷阱
表单数据reactive属性访问方便
Props 传递ref包装值更安全

三、computed:计算属性

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可写计算属性
const displayName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last
  },
})

// 使用
// displayName.value = 'Jane Smith'  // 自动拆分赋值

computed vs method:

  • computed 有缓存——依赖不变时不会重新计算
  • method 每次渲染都会执行
  • 需要缓存的结果用 computed,不需要缓存的操作用 function

四、watch 与 watchEffect:侦听器

4.1 watch:显式追踪

import { ref, watch } from 'vue'

const query = ref('')
const results = ref([])

// 监听单个 ref
watch(query, async (newVal, oldVal) => {
  results.value = await searchApi(newVal)
}, { immediate: false })  // immediate: 创建时立即执行一次

// 监听多个源
watch([query, page], async ([newQuery, newPage]) => {
  results.value = await searchApi(newQuery, newPage)
})

// 监听深层对象(默认只监听引用变化)
watch(
  () => state.user,       // getter 返回要监听的对象
  (newVal, oldVal) => { /* ... */ },
  { deep: true }           // 深度监听
)

// 获取旧值(对象类型需注意结构问题)
watch(user, (newVal, oldVal) => {
  console.log(`${oldVal?.name} → ${newVal?.name}`)
})

4.2 watchEffect:自动追踪

import { ref, watchEffect } from 'vue'

const count = ref(0)
const multiplier = ref(2)

// 自动追踪所有响应式依赖
watchEffect(() => {
  console.log(count.value * multiplier.value)  // 追踪 count 和 multiplier
})

// 带清理
watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log(count.value)
  }, 1000)

  onCleanup(() => {
    clearInterval(timer)  // 下一次执行或组件卸载时清理
  })
})

watch vs watchEffect:

特性watchwatchEffect
依赖追踪显式声明自动追踪
旧值访问✅ 可以❌ 不可以
立即执行需要配置 immediate✅ 默认执行
适用精确控制副作用集成

五、生命周期钩子

import {
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted,
  onErrorCaptured,
} from 'vue'

// 挂载
onBeforeMount(() => console.log('组件挂载前'))
onMounted(() => console.log('组件已挂载'))

// 更新
onBeforeUpdate(() => console.log('组件更新前'))
onUpdated(() => console.log('组件已更新'))

// 卸载
onBeforeUnmount(() => console.log('组件卸载前'))
onUnmounted(() => console.log('组件已卸载'))

// 错误捕获
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err, info)
  return false  // true: 继续向上传播,false: 阻止传播
})

六、组件通信

6.1 Props + Emits

<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
const title = 'Hello'
const handleUpdate = (val) => console.log(val)
</script>

<template>
  <Child :title="title" @update="handleUpdate" />
</template>

<!-- Child.vue -->
<script setup>
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 },
})

const emit = defineEmits(['update', 'delete'])

const notifyParent = () => {
  emit('update', 'new value')
}
</script>

6.2 自定义 v-model

<!-- CustomInput.vue -->
<script setup>
const model = defineModel()  // Vue 3.4+ 简化写法
// 等价于:const props = defineProps(['modelValue'])
//        const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input v-model="model" />
</template>

<!-- 使用 -->
<CustomInput v-model="text" />

6.3 provide / inject

// 祖先组件
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
})

// 后代组件
import { inject } from 'vue'

const theme = inject('theme', 'light')  // 默认值 'light'
const toggleTheme = inject('toggleTheme')

七、自定义 Composable:逻辑复用

7.1 Composable 规范

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data  // 返回 ref,方便解构
}

// 使用
const userPref = useLocalStorage('user-pref', { theme: 'dark', lang: 'zh' })
userPref.value.theme = 'light'  // 自动同步到 localStorage

7.2 常用 Composable 示例

// useAsync.ts —— 异步数据获取
export function useAsync<T>(fn: () => Promise<T>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fn()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, execute }
}

// useEventListener.ts —— 事件监听
export function useEventListener(
  target: EventTarget,
  event: string,
  callback: EventListener
) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

八、完整组件示例

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

// ========== Props / Emits ==========
const props = defineProps<{
  initialQuery: string
  pageSize: number
}>()

const emit = defineEmits<{
  search: [results: SearchResult[]]
}>()

// ========== State ==========
const query = ref(props.initialQuery)
const page = ref(1)
const results = ref<SearchResult[]>([])
const loading = ref(false)
const error = ref('')

// ========== Computed ==========
const hasNextPage = computed(() => results.value.length >= props.pageSize)

// ========== Methods ==========
async function fetchResults() {
  loading.value = true
  error.value = ''
  try {
    const data = await searchApi(query.value, page.value, props.pageSize)
    results.value = data
    emit('search', data)
  } catch (e) {
    error.value = (e as Error).message
  } finally {
    loading.value = false
  }
}

function nextPage() {
  page.value++
}

// ========== Watchers ==========
watch(query, () => {
  page.value = 1  // 查询变化时重置页码
  fetchResults()
}, { debounce: 300 })  // 防抖(需实现或使用第三方库)

watch(page, fetchResults)

// ========== Lifecycle ==========
onMounted(() => {
  fetchResults()
})
</script>

<template>
  <div class="search">
    <input v-model="query" placeholder="Search..." />
    <p v-if="loading">Loading...</p>
    <p v-else-if="error" class="error">{{ error }}</p>
    <ul v-else>
      <li v-for="item in results" :key="item.id">{{ item.name }}</li>
    </ul>
    <button v-if="hasNextPage" @click="nextPage" :disabled="loading">
      Next Page
    </button>
  </div>
</template>

九、深入响应式原理:Proxy vs Object.defineProperty

Vue 3 的响应式系统基于 ES6 Proxy,与 Vue 2 的 Object.defineProperty 有本质区别:

特性Vue 2 (defineProperty)Vue 3 (Proxy)
拦截能力仅 getter/setterget/set/has/ownKeys/deleteProperty 等 13 种操作
新增/删除属性检测不到(需 Vue.set/Vue.delete)自动检测
数组索引/length部分支持(改写 7 个变异方法)原生支持
Map/Set/WeakMap不支持响应式原生支持
性能初始化时遍历所有属性定义 getter/setter懒拦截——访问时才建立依赖
// Vue 3 的 Proxy 实现简化示意
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)        // 收集依赖(当前活跃的 effect)
      return isObject(result) ? reactive(result) : result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key)    // 触发依赖更新
      }
      return result
    }
  })
}

Proxy 的懒拦截特性意味着:只有被模板或计算属性访问到的属性才是响应式的。一个 1000 个字段的对象,如果页面只用其中 3 个,Vue 3 只为这 3 个建立依赖追踪——Vue 2 则需要遍历全部。

shallowRef 与 shallowReactive

对于不需要深度响应式的大对象(如第三方库实例、DOM 元素),避免无意义的递归代理开销:

import { shallowRef, shallowReactive } from 'vue'

// shallowRef:值替换时触发更新,但内部属性不是响应式
const bigObject = shallowRef({
  nested: { deep: { value: 1 } }  // nested.deep.value 不是响应式
})
bigObject.value = { ... }  // ✅ 替换整个对象触发更新
bigObject.value.nested.deep.value = 2  // ❌ 不会触发更新

// shallowReactive:顶层属性响应式,嵌套对象不做代理
const state = shallowReactive({
  title: 'Hello',       // ✅ 响应式
  config: { theme: 'dark' }  // ❌ config.theme 不是响应式
})

适用场景:集成 Three.js 场景对象、ECharts 实例、或者从后端一次性加载的参考数据(不需要细粒度响应式修改)。

effectScope:批量控制副作用生命周期

高级场景下需要手动控制多个 watch/watchEffect/computed 的批量创建与销毁:

import { effectScope, watch, ref } from 'vue'

const scope = effectScope()

scope.run(() => {
  const count = ref(0)
  watch(count, (v) => console.log(v))
  // 这里的 watch 和 computed 都被绑定到 scope
})

// 组件卸载或条件变化时,一键停止所有副作用
scope.stop()  // 比逐个调用 stop 更高效

effectScope 在选项卡切换、弹窗打开关闭、条件渲染等场景下尤为有用——避免已隐藏的组件残留的副作用继续运行。


十、扩展组合:Teleport、Suspense 与 Composable

<Teleport> 跨层级渲染

将组件的内容渲染到 DOM 的其他位置,常用于模态框、下拉菜单、Toast 通知:

<script setup>
const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">打开</button>

  <!-- Teleport:内容实际渲染到 body 末尾,避免 z-index 层级问题 -->
  <Teleport to="body">
    <div v-if="showModal" class="modal-overlay">
      <div class="modal">
        <h2>确认操作</h2>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>
场景是否用 Teleport原因
全屏模态框✅避免被父级 overflow: hidden 裁剪
Toast 通知✅需要固定在视口顶层
下拉菜单可选简单场景 CSS absolute 足够
表单控件❌保持 DOM 层级一致性

<Suspense> 异步依赖等待

等待异步组件或异步 setup 完成后再渲染:

<template>
  <Suspense>
    <template #default>
      <!-- 异步组件加载完成后显示 -->
      <AsyncDashboard />
    </template>
    <template #fallback>
      <!-- 加载状态 -->
      <div class="skeleton-loader">正在加载仪表盘...</div>
    </template>
  </Suspense>
</template>
<!-- AsyncDashboard.vue -->
<script setup>
// 顶层 await:组件等待数据加载完成再渲染
const data = await fetchDashboardData()
</script>

注意:Suspense 目前(Vue 3.4+)是实验性稳定功能(experimental → stable),但错误边界处理仍需手动包装。


十一、性能优化战术

v-memo:细粒度跳过更新

<template>
  <div v-memo="[item.id, item.status]">
    <!-- 只有当 id 或 status 变化时才重新渲染 -->
    <ComplexComponent :data="item" />
  </div>
</template>

大数据列表(> 1000 行)配合 v-memo 可显著减少无关更新。与 v-for 联用:

<div v-for="item in list" :key="item.id" v-memo="[item.id, item.score]">
  {{ item.name }} - {{ item.score }}
</div>

markRaw 与 readonly:明确响应式边界

import { markRaw, readonly } from 'vue'

// markRaw:对象永远不变成响应式代理(适合外部库实例)
const chart = markRaw(new EChartsInstance())

// readonly:响应式但只读(传递给子组件防止意外修改)
const readonlyState = readonly(reactive({ count: 0 }))
// readonlyState.count++  // ❌ 运行时 warning,开发时即被拒绝

computed 缓存策略

computed 只在依赖变化时重新计算。但如果依赖是一个对象且内部属性频繁变化,可以考虑拆解为多个细粒度的 computed:

// ❌ 任何 user 内部变化都触发 fullProfile 重算
const fullProfile = computed(() => {
  return `${user.value.name} | ${user.value.age} | ${user.value.bio}`
})

// ✅ 每个字段独立缓存,只有对应字段变化才触发
const displayName = computed(() => user.value.name)
const displayAge = computed(() => user.value.age)

十二、Composition API vs Options API:深度对比

维度Options APIComposition API
代码组织按选项(data/computed/methods)按逻辑关注点(useFeatureA, useFeatureB)
逻辑复用Mixins(命名冲突、来源不明)Composable(显式导入、函数级复用)
TypeScript 支持需 defineComponent 泛型<script setup> + 纯类型推断
新手友好度更直观(看选项名就知道是什么)陡峭一些,但更灵活
大型组件逻辑分散在多个选项中逻辑内聚,维护性更强
** tree-shaking**组件整体可按函数级别

选型建议:

  • 新项目 → Composition API(Vue 3 默认推荐)
  • 小/中型组件(< 200 行)→ 两者皆可
  • 需要大量逻辑复用 → Composition API(Composable)
  • 团队刚从 Vue 2 迁移 → 可保留 Options API,逐步迁移

十三、与 Pinia 的配合模式

Pinia 是 Vue 官方推荐的状态管理库,与 Composition API 无缝集成:

// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const history = ref<number[]>([])

  // getters
  const doubleCount = computed(() => count.value * 2)

  // actions
  function increment() {
    history.value.push(count.value)
    count.value++
  }

  function reset() {
    count.value = 0
    history.value = []
  }

  return { count, doubleCount, history, increment, reset }
})
<!-- Component.vue -->
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const store = useCounterStore()

// storeToRefs:只解构出响应式状态(actions 直接取用)
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
</script>
Pinia 特性Composition API 配合
Setup Store与 ref/computed 完全一致,无需学习新 API
$reset自定义 reset() action,比 Vuex 更灵活
插件系统可编写 pinia-plugin-persistedstate 替代 useLocalStorage composable
DevToolsVue DevTools 中对 store 状态的时轴调试

常见反模式

反模式问题修正
在 Pinia store 外直接修改 store.count失去追踪和 DevTools 支持始终通过 action 修改
过度使用全局 store全局状态越多,耦合越重优先用 props/emit,局部状态优先
在 store 中放入 UI 状态(如 isModalOpen)污染业务逻辑UI 状态留在组件,业务状态进 store

常见问题(FAQ)

<script setup> 里怎么定义组件名?

<script setup>
defineOptions({ name: 'MyComponent' })
</script>

ref 和 reactive 可以混用吗?

可以。常见模式是用 reactive 管理局部状态,ref 管理需要传递或解构的值。

为什么 watch 监听 reactive 对象必须用 getter?

const state = reactive({ count: 0 })

// ❌ 无效:监听的是 state 的引用,不会触发
watch(state, () => {})

// ✅ 有效:getter 返回内部值
watch(() => state.count, (newVal) => {})

// ✅ 或用 deep: true
watch(() => state, handler, { deep: true })

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. GraphQL BFF 与微前端:多前端团队的 Schema 分片与协作模式
  2. 前端 GraphQL 客户端集成:Apollo Client、Relay 与 urql 选型
  3. 前端性能调试实战:从首屏加载到运行时瓶颈