xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • Vue Composables 原理和使用

Vue Composables 原理和使用

Vue 3 引入的 Composition API 彻底改变了我们编写 Vue 组件的方式,而 Composables 则是基于此 API 构建的可复用逻辑单元。它们让我们能够提取和重用有状态逻辑,同时保持组件间的状态隔离,大大提升了代码的模块化和可维护性。

1. Composition API:Vue 3 的响应式核心

Composition API 是 Vue 3 引入的一套新 API,用于组织和复用组件逻辑。它通过 setup 函数来定义组件的逻辑,使得我们可以更灵活地使用 JavaScript 特性来组织代码。

1.1 核心概念

  • setup 函数: 是 Composition API 的入口,在组件实例创建之前执行。它返回一个对象,该对象中的属性和方法将被暴露给模板使用。
  • 响应式 API: 如 ref、reactive、computed 和 watch,用于创建和管理响应式状态。
  • 生命周期钩子: 如 onMounted、onUpdated 和 onUnmounted,用于处理组件的生命周期事件。

1.2 示例:基础用法

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref } from 'vue'

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

在这个示例中,setup 函数返回的 count 和 increment 被暴露给模板使用。我们使用 ref 创建了一个响应式变量 count,并定义了一个 increment 方法来增加计数。

2. Composition:代码组织的理念

Composition 是一种将逻辑和功能分离成独立的、可重用部分的概念。在 Vue 3 中,Composition API 实现了这一概念,使得我们可以将组件逻辑组织成独立的模块,称为 Composables。

2.1 Composition 的优势

  • 模块化: 将逻辑分离成独立模块,增强代码的可读性和可维护性。
  • 复用性: 独立的逻辑模块可以在多个组件中复用,减少代码重复。
  • 灵活性: 更灵活地组合和管理逻辑,使代码更易于扩展和修改。

2.2 示例:逻辑分离

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

export function useCounter() {
  const count = ref(0)
  
  const increment = () => {
    count.value++
  }
  
  return {
    count,
    increment
  }
}
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { useCounter } from './useCounter'

export default {
  setup() {
    const { count, increment } = useCounter()
    
    return {
      count,
      increment
    }
  }
}
</script>

在这个示例中,我们将计数逻辑提取到 useCounter 函数中,使其成为一个独立的模块。然后在组件中使用这个模块,实现逻辑的复用。

3. Composables:可复用的逻辑函数

Composables 是使用 Composition API 创建的函数,用于封装和复用逻辑。它们通常以 use 开头,表示这是一个可复用的逻辑模块。例如,useCounter 是一个计数逻辑的 Composable。

3.1 Composables 的优势

  • 逻辑复用: 将逻辑封装在 Composables 中,可以在不同的组件中复用,减少代码重复。
  • 易于测试: 独立的逻辑模块更易于测试和调试。
  • 增强可维护性: 将复杂的逻辑分解成简单的、可复用的函数,提高代码的可维护性。

3.2 示例:数据获取 Composable

// useFetch.js
import { ref, onMounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  
  const fetchData = async () => {
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    }
  }
  
  onMounted(fetchData)
  
  return {
    data,
    error,
    fetchData
  }
}
<template>
  <div>
    <div v-if="error">{{ error.message }}</div>
    <div v-else-if="data">{{ data }}</div>
    <div v-else>Loading...</div>
  </div>
</template>

<script>
import { useFetch } from './useFetch'

export default {
  setup() {
    const { data, error } = useFetch('https://api.example.com/data')
    
    return {
      data,
      error
    }
  }
}
</script>

在这个示例中,我们创建了一个 useFetch Composable,用于封装数据获取的逻辑。然后在组件中使用这个 Composable,实现数据获取功能的复用。

4. Composables 与 Mixins 的对比

在 Vue 3 引入 Composables 之前,开发者通常使用 Mixins 来实现代码复用。虽然 Mixins 有一定的作用,但它们存在几个明显的问题,而 Composables 则提供了更好的解决方案。

4.1 Mixins 的问题

  • 命名冲突: 多个 Mixins 可能会使用相同的属性名,导致意外覆盖。
  • 隐式依赖: 很难清楚地知道 Mixin 使用了哪些属性和方法。
  • 关系不明确: 当多个 Mixins 交互时,逻辑关系变得复杂难懂。

4.2 Composables 的优势

  • 显式依赖: 通过导入和调用函数,依赖关系更加明确。
  • 命名空间: 可以重命名解构的变量,避免命名冲突。
  • 类型支持: 更好的 TypeScript 支持,提供完整的类型推断。

4.3 示例:表单验证对比

Mixin 方式:

// formValidation.js
export const formValidationMixin = {
  data() {
    return {
      formData: {
        username: '',
        password: '',
      },
      formErrors: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    validateForm() {
      this.formErrors = {};
  
      if (!this.formData.username.trim()) {
        this.formErrors.username = 'Username is required.';
      }
  
      if (!this.formData.password.trim()) {
        this.formErrors.password = 'Password is required.';
      }
   
      return Object.keys(this.formErrors).length === 0;
    },
  },
};

Composable 方式:

// formValidation.js
import { reactive } from 'vue';

export function useFormValidation() {
  const state = reactive({
    formData: {
      username: '',
      password: '',
    },
    formErrors: {
      username: '',
      password: '',
    },
  });

  function validateForm() {
    state.formErrors = {};
    if (!state.formData.username.trim()) {
      state.formErrors.username = 'Username is required.';
    }
    if (!state.formData.password.trim()) {
      state.formErrors.password = 'Password is required.';
    }
    return Object.keys(state.formErrors).length === 0;
  }

  return {
    state,
    validateForm,
  };
}

Composable 方式提供了更清晰的代码结构和更好的类型支持。

5. 高级 Composable 模式

5.1 参数化 Composables

Composables 可以接受参数,使其更加灵活和可配置:

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

export function useStringFormatter(data, separator) {
  const formatted = ref('')
  
  watchEffect(() => {
    formatted.value = toValue(data).replace(/\s+/g, toValue(separator))
  })
  
  return {
    formatted
  }
}

在这个示例中,useStringFormatter Composable 接受 data 和 separator 参数,并使用 watchEffect 来响应参数的变化。

5.2 共享状态 Composables

有时我们需要在多个组件之间共享状态,可以使用闭包和 EffectScope 来实现:

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

export function createSharedComposable(composable) {
  let subscribers = 0
  let state
  let scope
  
  const dispose = () => {
    subscribers--
    if (scope && subscribers <= 0) {
      scope.stop()
      state = undefined
      scope = undefined
    }
  }
  
  return (...args) => {
    subscribers++
    if (!scope) {
      scope = effectScope(true)
      state = scope.run(() => composable(...args))
    }
    onScopeDispose(dispose)
    return state
  }
}

// 使用示例
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  
  return { count, increment }
}

const useSharedCounter = createSharedComposable(useCounter)

这个 createSharedComposable 函数可以将普通的 Composable 转换为共享的 Composable,在多个组件之间共享同一份状态和副作用。

5.3 生命周期感知的 Composables

有时我们需要在 Composables 中感知组件的生命周期,可以创建专门的生命周期 Composable:

// composables/useLifecycle.ts
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

export type LifecycleState = 
  'beforeCreate' | 'created' | 'beforeMount' |
  'mounted' | 'beforeUpdate' | 'updated' |
  'beforeUnmount' | 'unmounted'

export function useLifecycle() {
  const lifecycle = ref<LifecycleState>('beforeCreate')
  
  // 生命周期映射表
  const hooks = {
    created: () => lifecycle.value = 'created',
    beforeMount: () => lifecycle.value = 'beforeMount',
    mounted: () => lifecycle.value = 'mounted',
    beforeUpdate: () => lifecycle.value = 'beforeUpdate',
    updated: () => lifecycle.value = 'updated',
    beforeUnmount: () => lifecycle.value = 'beforeUnmount',
    unmounted: () => lifecycle.value = 'unmounted'
  }

  // 注册生命周期钩子
  onMounted(hooks.mounted)
  onUpdated(hooks.updated)
  onUnmounted(hooks.unmounted)

  // 返回带重置功能的对象
  return {
    lifecycle,
    reset: () => lifecycle.value = 'beforeCreate'
  }
}

这个 useLifecycle Composable 可以追踪组件的生命周期状态,在调试和特定生命周期操作时非常有用。

6. TypeScript 集成

TypeScript 为 Composables 提供了强大的类型支持,增强了代码的可靠性和开发体验。

6.1 基础类型定义

// types.ts
export interface FetchResult<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  isLoading: Ref<boolean>
}

// useFetch.ts
import { ref } from 'vue'

export function useFetch<T>(url: string) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  const execute = async () => {
    isLoading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json() as T
    } catch (err) {
      error.value = err as Error
    } finally {
      isLoading.value = false
    }
  }

  return { data, error, isLoading, execute }
}

6.2 高级类型技巧

// 泛型参数约束
export function useFetch<T extends object>(url: string) {
  // 约束泛型必须是对象类型
}

// 错误类型细化
class ApiError extends Error {
  statusCode: number
  
  constructor(message: string, statusCode: number) {
    super(message)
    this.statusCode = statusCode
  }
}

// 配置对象模式
interface FetchOptions {
  immediate?: boolean
  timeout?: number
}

function useFetch<T>(url: string, options?: FetchOptions) {
  // 实现配置处理
}

// 请求取消支持
const controller = new AbortController()

fetch(url, { 
  signal: controller.signal 
})

// 组件卸载时取消请求
onUnmounted(() => controller.abort())

TypeScript 集成提供了编译时错误预防、IDE 智能提示、接口变更快速反馈和代码自文档化等优势。

7. VueUse:实用的 Composable 库

VueUse 是一个基于 Vue 3 Composition API 的实用工具库,提供了 200+ 可复用的组合式函数,覆盖状态管理、浏览器 API、动画、响应式布局等常见开发场景。

7.1 常用功能示例

useFetch: 简化 HTTP 请求处理

<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '@vueuse/core'

const { data, error, isFetching } = useFetch('https://api.example.com/data')

const fetchData = async () => {
    if (isFetching.value) return
    await data.value // 等待数据加载完成
    console.log(data.value)
}

if (error.value) {
    console.error('Fetch Error:', error.value)
}
</script>

useStorage: 本地存储管理

<script setup lang="ts">
import { useStorage } from '@vueuse/core'

const name = useStorage('user-name', '')
</script>

useMouse: 鼠标位置追踪

<script setup lang="ts">
import { useMouse } from '@vueuse/core'

const { x, y } = useMouse()
</script>

useDark: 暗黑模式切换

<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

7.2 VueUse 的优势

  • 减少重复代码: 200+ 函数覆盖 90% 常见场景
  • 提升开发效率: 从编写逻辑到测试的全流程优化
  • 增强代码质量: 社区维护的稳定实现
  • 拥抱 Vue3 生态: 与 Composition API 深度集成

8. 最佳实践与注意事项

8.1 命名约定

Composable 函数通常以 use 开头,表示这是一个可复用的逻辑模块。这种命名约定有助于区分普通函数和 Composables。

8.2 响应式状态管理

  • 优先使用 ref 而不是 reactive,因为 ref 保持响应性并支持解构
  • 对于复杂对象,可以使用 reactive 但要注意失去响应性的风险
  • 使用 computed 来处理派生状态

8.3 副作用管理

  • 使用 watch 和 watchEffect 来响应状态变化
  • 确保清理副作用,防止内存泄漏
  • 使用 onUnmounted 或 onScopeDispose 来清理资源

8.4 性能优化

  • 按需引入 Composables,避免不必要的代码加载
  • 使用防抖和节流优化频繁操作
  • 考虑使用共享 Composables 来避免重复计算

8.5 测试策略

  • 独立测试 Composables,确保逻辑正确性
  • 使用 Vue Test Utils 或专门的测试库
  • 模拟依赖项,确保测试的隔离性

9. 实战案例:完整的待办事项应用

让我们通过一个完整的待办事项应用来展示 Composables 的实际应用:

// composables/useTodoList.ts
import { ref, computed } from 'vue'

export interface Todo {
  id: number
  text: string
  completed: boolean
  createdAt: Date
}

export function useTodoList() {
  const todos = ref<Todo[]>([])
  const filter = ref<'all' | 'active' | 'completed'>('all')
  
  const addTodo = (text: string) => {
    if (!text.trim()) return
    
    todos.value.push({
      id: Date.now(),
      text: text.trim(),
      completed: false,
      createdAt: new Date()
    })
  }
  
  const removeTodo = (id: number) => {
    todos.value = todos.value.filter(todo => todo.id !== id)
  }
  
  const toggleTodo = (id: number) => {
    const todo = todos.value.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  const clearCompleted = () => {
    todos.value = todos.value.filter(todo => !todo.completed)
  }
  
  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 stats = computed(() => {
    const total = todos.value.length
    const completed = todos.value.filter(todo => todo.completed).length
    const active = total - completed
    
    return { total, completed, active }
  })
  
  return {
    todos: filteredTodos,
    filter,
    stats,
    addTodo,
    removeTodo,
    toggleTodo,
    clearCompleted
  }
}
<template>
  <div class="todo-app">
    <h1>待办事项</h1>
    
    <div class="add-form">
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo(newTodo)"
        placeholder="添加新任务..."
        class="todo-input"
      >
      <button @click="addTodo(newTodo)" class="add-btn">添加</button>
    </div>
    
    <div class="filters">
      <button 
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        全部 ({{ stats.total }})
      </button>
      <button 
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        待办 ({{ stats.active }})
      </button>
      <button 
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        已完成 ({{ stats.completed }})
      </button>
    </div>
    
    <ul class="todo-list">
      <li v-for="todo in todos" :key="todo.id" class="todo-item">
        <input 
          type="checkbox" 
          :checked="todo.completed" 
          @change="toggleTodo(todo.id)"
          class="todo-checkbox"
        >
        <span :class="{ completed: todo.completed }" class="todo-text">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(todo.id)" class="delete-btn">删除</button>
      </li>
    </ul>
    
    <div v-if="stats.completed > 0" class="clear-container">
      <button @click="clearCompleted" class="clear-btn">
        清除已完成 ({{ stats.completed }})
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useTodoList } from './composables/useTodoList'

const newTodo = ref('')
const { todos, filter, stats, addTodo, removeTodo, toggleTodo, clearCompleted } = useTodoList()

const addTodoAndClear = (text: string) => {
  addTodo(text)
  newTodo.value = ''
}
</script>

这个示例展示了如何使用 Composable 来管理复杂的应用状态,包括添加、删除、切换和过滤待办事项等功能。

10. 总结

Vue Composables 是 Vue 3 中一个强大的特性,它彻底改变了我们组织和复用代码的方式。通过本文的探讨,我们可以总结出以下几点关键见解:

10.1 核心价值

  • 逻辑复用: Composables 允许我们将有状态逻辑提取到可重用的函数中,减少代码重复和提高一致性。
  • 代码组织: 通过功能而不是选项类型来组织代码,使相关逻辑保持在一起,提高可读性和可维护性。
  • 类型安全: 与 TypeScript 的深度集成提供了更好的开发体验和更可靠的代码。

10.2 适用场景

  • 复杂状态逻辑: 当组件包含复杂的状态管理逻辑时,使用 Composables 可以更好地组织代码。
  • 跨组件复用: 当多个组件需要共享相同或类似的逻辑时,Composables 提供了一种干净的解决方案。
  • 第三方集成: 当需要与非 Vue 库或 API 集成时,Composables 可以封装复杂性。

10.3 最佳实践

  1. 从简单的 Composables 开始,逐步扩展到更复杂的场景
  2. 合理使用响应式 API,理解 ref 和 reactive 的区别和适用场景
  3. 注意副作用管理,确保及时清理资源防止内存泄漏
  4. 充分利用 TypeScript 提供类型安全和更好的开发体验
  5. 考虑使用 VueUse 等现有库,避免重复造轮子
最后更新: 2025/9/23 09:31