前言

Vue 3.5 带来了许多令人兴奋的新特性和性能优化,其中 Composition API 已经成为 Vue 开发的首选方式。它提供了更灵活的代码组织方式、更好的 TypeScript 支持以及更强大的逻辑复用能力。本文将带你深入理解 Composition API 的核心概念和高级用法。

响应式基础:ref 与 reactive

ref:基础响应式引用

ref 是 Composition API 中最基本的响应式 API,适用于任何类型的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { ref } from 'vue'

// 基础用法
const count = ref(0)
const message = ref('Hello Vue')
const isVisible = ref(false)

// 在模板中使用时会自动解包
// <template>{{ count }}</template>

// 在 JavaScript 中访问需要 .value
console.log(count.value) // 0
count.value = 1

最佳实践:ref 的命名规范

1
2
3
4
5
6
7
8
// ✅ 好的命名
const isLoading = ref(false)
const errorMessage = ref('')
const userCount = ref(0)

// ❌ 避免的命名
const data = ref(null)  // 太通用
const flag = ref(true)  // 不够明确

reactive:对象响应式代理

对于对象类型的数据,reactive 提供了更自然的访问方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { reactive } from 'vue'

const state = reactive({
  user: {
    name: 'John',
    email: 'john@example.com'
  },
  settings: {
    theme: 'dark',
    language: 'zh-CN'
  }
})

// 直接访问,无需 .value
console.log(state.user.name)
state.settings.theme = 'light'

reactive 的注意事项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ✅ 推荐:始终使用响应式对象
const form = reactive({
  username: '',
  password: '',
  email: ''
})

// ❌ 避免:解构会丢失响应性
const { username } = form  // username 不再是响应式的

// ✅ 正确的解构方式
import { toRefs } from 'vue'
const { username, password, email } = toRefs(form)

计算属性:computed 的强大功能

基础计算属性

1
2
3
4
5
6
import { ref, computed } from 'vue'

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

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

可写的计算属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

// 可以直接赋值
fullName.value = 'Jane Smith'

计算属性的缓存与性能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// expensiveCalculation 只在依赖项变化时执行
const expensiveResult = computed(() => {
  console.log('执行复杂计算...')
  return heavyCalculation(data.value)
})

// 避免在计算属性中进行副作用操作
const badExample = computed(() => {
  // ❌ 不要在 computed 中修改其他响应式数据
  otherValue.value = processData(data.value)
  return processedData
})

侦听器:watch 与 watchEffect

watch:精确的响应式侦听

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { ref, watch } from 'vue'

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

// 基础用法
watch(searchQuery, (newQuery, oldQuery) => {
  console.log(`查询从 "${oldQuery}" 变为 "${newQuery}"`)
  fetchResults(newQuery)
})

// 侦听多个源
watch([searchQuery, filterType], ([newQuery, newFilter], [oldQuery, oldFilter]) => {
  fetchFilteredResults(newQuery, newFilter)
})

watch 的配置选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
watch(
  source,
  (newValue, oldValue) => {
    // 回调函数
  },
  {
    immediate: true,  // 立即执行一次
    deep: true,       // 深度侦听
    flush: 'post'     // 'pre' | 'post' | 'sync'
  }
)

watchEffect:自动追踪依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { watchEffect, ref } from 'vue'

const userId = ref(1)
const userData = ref(null)

// 自动追踪回调中使用的响应式数据
watchEffect(async () => {
  console.log(`获取用户 ${userId.value} 的数据`)
  userData.value = await fetchUser(userId.value)
})

// 当 userId 变化时,effect 会自动重新运行

清理副作用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    updateData()
  }, 1000)

  // 注册清理函数
  onCleanup(() => {
    clearInterval(timer)
  })
})

组合式函数:逻辑复用的艺术

创建可复用的组合式函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)
  const isEven = computed(() => count.value % 2 === 0)

  const increment = (step = 1) => {
    count.value += step
  }

  const decrement = (step = 1) => {
    count.value -= step
  }

  const reset = () => {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    isEven,
    increment,
    decrement,
    reset
  }
}

在组件中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubled }}</p>
    <p>偶数: {{ isEven ? '是' : '否' }}</p>
    <button @click="increment()">增加</button>
    <button @click="decrement()">减少</button>
    <button @click="reset()">重置</button>
  </div>
</template>

<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, isEven, increment, decrement, reset } = useCounter(10)
</script>

高级组合式函数示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// composables/useFetch.js
import { ref, watchEffect } from 'vue'

export function useFetch(url, options = {}) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url.value || url, options)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  // 如果 url 是响应式的,自动重新获取
  if (typeof url === 'object') {
    watchEffect(execute)
  } else {
    execute()
  }

  return {
    data,
    error,
    loading,
    execute
  }
}

Vue 3.5 新特性探索

defineModel 简化双向绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- Vue 3.5 之前 -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const updateValue = (event) => {
  emit('update:modelValue', event.target.value)
}
</script>

<!-- Vue 3.5 新语法 -->
<script setup>
const modelValue = defineModel()
</script>

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

Teleport 的增强用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
  <div>
    <button @click="showModal = true">打开模态框</button>

    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>模态框标题</h2>
          <p>内容被传送到 body 元素下</p>
          <button @click="showModal = false">关闭</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  max-width: 500px;
}
</style>

性能优化技巧

使用 shallowRef 和 shallowReactive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { shallowRef, shallowReactive, triggerRef } from 'vue'

// 对于大型对象,避免深度响应式
const largeData = shallowRef({
  // 大量嵌套数据
})

// 修改深层属性需要手动触发
largeData.value.deep.property = 'new value'
triggerRef(largeData)

// 或者使用 markRaw 标记不需要响应式的对象
import { markRaw } from 'vue'
const staticConfig = markRaw({
  // 静态配置,不需要响应式
})

使用 readonly 保护数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { reactive, readonly } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue'
})

// 提供只读版本给子组件
const readonlyState = readonly(state)

// 子组件无法修改,确保数据流向清晰

常见陷阱与解决方案

陷阱 1:解构失去响应性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const state = reactive({ count: 0 })

// ❌ 错误:count 不再是响应式的
const { count } = state

// ✅ 解决方案 1:使用 toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)

// ✅ 解决方案 2:直接使用 state.count
// 在模板中:{{ state.count }}

陷阱 2:在 watch 中无限循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const data = ref(0)

// ❌ 可能导致无限循环
watch(data, () => {
  data.value = data.value + 1
})

// ✅ 使用 nextTick 或条件判断
watch(data, async () => {
  await nextTick()
  if (someCondition) {
    data.value = data.value + 1
  }
})

陷阱 3:异步操作的竞态条件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const data = ref(null)

// ❌ 多个请求可能导致竞态
watch(searchQuery, async () => {
  data.value = await fetchResults(searchQuery.value)
})

// ✅ 使用取消令牌或 AbortController
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
  const controller = new AbortController()

  onCleanup(() => {
    controller.abort()
  })

  data.value = await fetchResults(newQuery, {
    signal: controller.signal
  })
})

总结

Vue 3.5 的 Composition API 为我们提供了强大的工具来构建可维护、可复用的前端应用。通过合理使用 ref、reactive、computed、watch 等核心 API,我们可以写出更加清晰和高效的代码。

关键要点:

  1. 理解 ref 和 reactive 的使用场景和区别
  2. 善用 computed 进行数据派生和缓存
  3. 合理使用 watch 和 watchEffect 处理副作用
  4. 通过组合式函数实现逻辑复用
  5. 注意常见的性能陷阱和优化技巧

掌握 Composition API 不仅能提升你的 Vue 开发技能,更能让你在构建大型应用时游刃有余。


相关资源: