Vue3 组合式 API 实战指南

目录

前言

Vue3 的发布带来了全新的组合式 API(Composition API),它为我们提供了一种更灵活、更高效的组织组件逻辑的方式。相比于 Vue2 的选项式 API(Options API),组合式 API 允许我们按照逻辑关注点组织代码,而不是按照选项类型。本文将深入探讨组合式 API 的核心概念、实践技巧以及实战应用,帮助你快速掌握这一强大的特性。

组合式 API 基础

setup 函数

组合式 API 的核心是 setup 函数,它在组件创建之前执行,是组合式 API 的入口点。

import { ref, onMounted } from 'vue'

export default {
  setup() {
    // 声明响应式状态
    const count = ref(0)
    
    // 声明方法
    function increment() {
      count.value++
    }
    
    // 生命周期钩子
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    // 返回值会暴露给模板和其他选项
    return {
      count,
      increment
    }
  }
}

script setup 语法糖

Vue3.2 引入了 <script setup> 语法糖,它是组合式 API 的编译时语法糖,使代码更简洁:

<script setup>
import { ref, onMounted } from 'vue'

// 声明响应式状态
const count = ref(0)

// 声明方法
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载')
})

// 无需返回,所有顶层变量和导入都会自动暴露给模板
</script>

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

响应式系统

ref 和 reactive

Vue3 提供了两个主要的响应式 API:refreactive

ref

ref 用于创建任何类型的响应式数据,它将值包装在一个带有 .value 属性的对象中:

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

在模板中使用时,无需使用 .value,Vue 会自动解包:

<template>
  <div>{{ count }}</div>
</template>
reactive

reactive 用于创建响应式对象:

import { reactive } from 'vue'

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

console.log(state.count) // 0
state.count++

计算属性

使用 computed 函数创建计算属性:

import { ref, computed } from 'vue'

const count = ref(0)

// 只读计算属性
const doubleCount = computed(() => count.value * 2)

// 可写计算属性
const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

监听器

使用 watchwatchEffect 监听响应式数据的变化:

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

const count = ref(0)

// 监听单个数据源
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
}, { deep: true, immediate: true })

// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('数据变化了')
})

// 自动收集依赖并监听
watchEffect(() => {
  console.log(`count 的当前值是: ${count.value}`)
})

生命周期钩子

组合式 API 提供了一系列生命周期钩子函数,它们与选项式 API 中的钩子对应:

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

export default {
  setup() {
    onBeforeMount(() => {
      console.log('组件挂载前')
    })
    
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    onBeforeUpdate(() => {
      console.log('组件更新前')
    })
    
    onUpdated(() => {
      console.log('组件已更新')
    })
    
    onBeforeUnmount(() => {
      console.log('组件卸载前')
    })
    
    onUnmounted(() => {
      console.log('组件已卸载')
    })
    
    // 其他钩子...
  }
}

依赖注入

使用 provideinject 实现跨组件通信:

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

export default {
  setup() {
    const theme = ref('light')
    
    function toggleTheme() {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
    }
    
    // 提供值和方法
    provide('theme', theme)
    provide('toggleTheme', toggleTheme)
  }
}

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

export default {
  setup() {
    // 注入值和方法
    const theme = inject('theme')
    const toggleTheme = inject('toggleTheme')
    
    return {
      theme,
      toggleTheme
    }
  }
}

组合式函数(Composables)

组合式函数是组合式 API 最强大的特性之一,它允许我们将逻辑封装并在多个组件之间复用:

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    doubleCount
  }
}

// 在组件中使用
import { useCounter } from './composables/useCounter'

export default {
  setup() {
    const { count, increment, decrement, doubleCount } = useCounter(10)
    
    return {
      count,
      increment,
      decrement,
      doubleCount
    }
  }
}

常见的组合式函数示例

1. 使用 fetch 数据
// useFetch.js
import { ref, computed, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)
  
  const fetchData = async () => {
    loading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  watchEffect(() => {
    fetchData()
  })
  
  return { data, error, loading, refetch: fetchData }
}

// 使用
const { data, error, loading } = useFetch('https://api.example.com/data')
2. 管理本地存储
// useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
  
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

// 使用
const user = useLocalStorage('user', { name: '', age: 0 })

实战案例

待办事项应用

下面是一个使用组合式 API 构建的简单待办事项应用:

<script setup>
import { ref, computed, watch } from 'vue'

// 从本地存储加载待办事项
const loadTodos = () => {
  const saved = localStorage.getItem('todos')
  return saved ? JSON.parse(saved) : []
}

const todos = ref(loadTodos())
const newTodo = ref('')
const filter = ref('all') // all, active, completed

// 保存到本地存储
watch(todos, (newTodos) => {
  localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })

// 计算属性:过滤后的待办事项
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active':
      return todos.value.filter(todo => !todo.completed)
    case 'completed':
      return todos.value.filter(todo => todo.completed)
    default:
      return todos.value
  }
})

// 计算属性:剩余未完成项
const remaining = computed(() => {
  return todos.value.filter(todo => !todo.completed).length
})

// 添加新待办事项
function addTodo() {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: newTodo.value.trim(),
      completed: false
    })
    newTodo.value = ''
  }
}

// 移除待办事项
function removeTodo(todo) {
  const index = todos.value.findIndex(t => t.id === todo.id)
  if (index !== -1) {
    todos.value.splice(index, 1)
  }
}

// 清除已完成项
function clearCompleted() {
  todos.value = todos.value.filter(todo => !todo.completed)
}
</script>

<template>
  <div class="todo-app">
    <h1>待办事项</h1>
    
    <div class="add-todo">
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo" 
        placeholder="添加新待办事项..."
      />
      <button @click="addTodo">添加</button>
    </div>
    
    <div class="filters">
      <button 
        :class="{ active: filter === 'all' }" 
        @click="filter = 'all'"
      >
        全部
      </button>
      <button 
        :class="{ active: filter === 'active' }" 
        @click="filter = 'active'"
      >
        未完成
      </button>
      <button 
        :class="{ active: filter === 'completed' }" 
        @click="filter = 'completed'"
      >
        已完成
      </button>
    </div>
    
    <ul class="todo-list">
      <li v-for="todo in filteredTodos" :key="todo.id" :class="{ completed: todo.completed }">
        <input 
          type="checkbox" 
          v-model="todo.completed"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo)">删除</button>
      </li>
    </ul>
    
    <div class="todo-footer" v-if="todos.length > 0">
      <span>{{ remaining }} 项未完成</span>
      <button @click="clearCompleted" v-if="remaining < todos.length">
        清除已完成
      </button>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-todo {
  display: flex;
  margin-bottom: 20px;
}

.add-todo input {
  flex-grow: 1;
  padding: 8px;
  border: 1px solid #ddd;
}

button {
  padding: 8px 12px;
  background-color: #4CAF50;
  color: white;
  border: none;
  cursor: pointer;
  margin-left: 5px;
}

.filters {
  display: flex;
  margin-bottom: 15px;
}

.filters button {
  background-color: #f1f1f1;
  color: #333;
}

.filters button.active {
  background-color: #4CAF50;
  color: white;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #888;
}

.todo-list span {
  flex-grow: 1;
  margin: 0 10px;
}

.todo-footer {
  display: flex;
  justify-content: space-between;
  margin-top: 15px;
  color: #666;
}
</style>

性能优化

1. 使用 shallowRef 和 shallowReactive

当处理大型对象或不需要深层响应性时,使用浅层响应式 API 可以提高性能:

import { shallowRef, shallowReactive } from 'vue'

// 只有 state.count 是响应式的,state.nested 的变化不会触发更新
const state = shallowReactive({
  count: 0,
  nested: { value: 'hello' }
})

// 只有 .value 的替换是响应式的,.value.nested 的变化不会触发更新
const data = shallowRef({ nested: { value: 'hello' } })

2. 使用 v-once 和 v-memo

对于不需要频繁更新的内容,可以使用 v-oncev-memo 指令:

<template>
  <!-- 只渲染一次,后续更新会被跳过 -->
  <div v-once>{{ expensiveComputation() }}</div>
  
  <!-- 只有当 id 变化时才会重新渲染 -->
  <div v-memo="[item.id]">
    <span>{{ item.name }}</span>
    <span>{{ item.description }}</span>
  </div>
</template>

3. 使用 defineAsyncComponent 懒加载组件

import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() => 
  import('./components/HeavyComponent.vue')
)

与 TypeScript 结合

Vue3 提供了出色的 TypeScript 支持,以下是一些使用 TypeScript 的最佳实践:

定义 Props 类型

<script setup lang="ts">
import { defineProps } from 'vue'

// 方式一:使用运行时声明
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

// 方式二:使用类型声明(推荐)
interface Props {
  title: string
  count?: number
}

const props = defineProps<Props>()

// 方式三:使用类型声明并提供默认值
withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

定义 Emits 类型

<script setup lang="ts">
// 方式一:运行时声明
const emit = defineEmits(['update', 'delete'])

// 方式二:类型声明(推荐)
const emit = defineEmits<{
  (e: 'update', id: number, value: string): void
  (e: 'delete', id: number): void
}>()

// 使用
function handleUpdate(id: number) {
  emit('update', id, 'new value')
}
</script>

为 ref 和 reactive 添加类型

import { ref, reactive } from 'vue'

// 为 ref 添加类型
const count = ref<number>(0)
const user = ref<{ name: string; age: number } | null>(null)

// 为 reactive 添加类型
interface User {
  name: string
  age: number
}

const state = reactive<User>({
  name: 'John',
  age: 30
})

迁移策略

从 Vue2 迁移到 Vue3 的组合式 API 可以采取渐进式策略:

1. 混合使用选项式 API 和组合式 API

Vue3 允许在同一个组件中混合使用两种 API:

export default {
  data() {
    return {
      // 选项式 API 数据
      message: 'Hello'
    }
  },
  
  setup() {
    // 组合式 API 逻辑
    const count = ref(0)
    
    return {
      count
    }
  },
  
  methods: {
    // 选项式 API 方法
    greet() {
      console.log(this.message)
      console.log(this.count) // 可以访问 setup 返回的属性
    }
  }
}

2. 逐步重构

  1. 首先识别组件中的逻辑关注点
  2. 将每个关注点提取为组合式函数
  3. setup 中使用这些组合式函数
  4. 逐步移除选项式 API 代码

3. 使用 @vue/composition-api 在 Vue2 中提前体验

对于 Vue2 项目,可以使用 @vue/composition-api 插件提前体验组合式 API:

// main.js
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)

总结

Vue3 的组合式 API 为我们提供了一种全新的组织组件逻辑的方式,它具有以下优势:

  1. 逻辑复用:通过组合式函数(Composables)可以轻松实现逻辑复用
  2. 更好的类型推导:与 TypeScript 结合提供了出色的类型支持
  3. 更好的代码组织:按照逻辑关注点组织代码,而不是选项类型
  4. 更小的打包体积:支持更好的 Tree-shaking
  5. 更灵活的逻辑组合:可以根据需要组合各种功能

组合式 API 并不是要完全取代选项式 API,而是提供了另一种组织组件代码的方式。对于简单组件,选项式 API 仍然是一个很好的选择;而对于复杂组件或需要逻辑复用的场景,组合式 API 则能发挥其优势。

通过本指南的学习,相信你已经掌握了 Vue3 组合式 API 的核心概念和实践技巧。接下来,就是在实际项目中不断实践和探索,充分发挥组合式 API 的强大能力!


参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值