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 不支持基本类型
对象字面量reactiverefreactive 代码更简洁
需要解构赋值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>

常见问题(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」更多文章