vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3组合式API

深度解析Vue3组合式API的核心概念、使用场景和最佳实践

作者:天天进步2015

Vue 3带来的最大变革之一就是组合式API,不仅解决了Options API在大型项目中的局限性,更为开发者提供了一种更灵活、更优雅的代码组织方式,下面小编就为大家简单介绍一下吧

前言

Vue 3带来的最大变革之一就是组合式API(Composition API)。它不仅解决了Options API在大型项目中的局限性,更为开发者提供了一种更灵活、更优雅的代码组织方式。本文将深入探讨组合式API的核心概念、使用场景和最佳实践。

为什么需要组合式API

Options API的痛点

在Vue 2的Options API中,我们按照选项类型组织代码:

export default {
  data() {
    return {
      count: 0,
      user: null
    }
  },
  methods: {
    increment() {
      this.count++
    },
    fetchUser() {
      // 获取用户数据
    }
  },
  mounted() {
    this.fetchUser()
  }
}

这种方式在小型组件中运作良好,但随着组件复杂度增加,会遇到以下问题:

组合式API的优势

组合式API通过将相关逻辑组织在一起,完美解决了上述问题:

核心API详解

setup函数

setup是组合式API的入口点,在组件创建之前执行:

import { ref, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    function increment() {
      count.value++
    }
    
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    // 返回的内容会暴露给模板
    return {
      count,
      increment
    }
  }
}

响应式基础

ref:基本类型的响应式

ref为基本类型创建响应式引用:

import { ref } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 在JS中需要通过.value访问
console.log(count.value) // 0
count.value++

// 在模板中自动解包,不需要.value
// <div>{{ count }}</div>

reactive:对象的响应式

reactive为对象创建响应式代理:

import { reactive } from 'vue'

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

// 直接访问属性,不需要.value
state.count++
state.user.name = 'Vue 3'

ref vs reactive的选择

// 推荐:使用ref作为主要方式
const count = ref(0)
const user = ref({ name: 'Vue' })

// reactive适合:整体替换较少的对象
const form = reactive({
  username: '',
  password: ''
})

计算属性与侦听 器

computed:计算属性

import { ref, computed } from 'vue'

const count = ref(1)
const double = computed(() => count.value * 2)

// 可写的计算属性
const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
  get() {
    return firstName.value + lastName.value
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

watch:侦 听器

import { ref, watch } from 'vue'

const count = ref(0)

// 侦听单个ref
watch(count, (newValue, oldValue) => {
  console.log(`count变化:${oldValue} -> ${newValue}`)
})

// 侦听多个源
const firstName = ref('张')
const lastName = ref('三')

watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(`姓名:${newFirst}${newLast}`)
})

// 深度侦听
const state = ref({ count: 0 })
watch(state, (newValue) => {
  console.log('状态改变')
}, { deep: true })

watchEffect:自动追踪依赖

import { ref, watchEffect } from 'vue'

const count = ref(0)
const double = ref(0)

// 自动追踪响应式依赖
watchEffect(() => {
  double.value = count.value * 2
  console.log(`count: ${count.value}, double: ${double.value}`)
})

生命周期钩子

组合式API中的生命周期钩子:

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

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

组合函数(Composables)

组合函数是组合式API最强大的特性之一,它允许我们提取和复用有状态逻辑。

创建自定义组合函数

示例:鼠标位置追踪

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

使用组合函数:

import { useMouse } from './composables/useMouse'

export default {
  setup() {
    const { x, y } = useMouse()
    
    return { x, y }
  }
}

示例:网络请求

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

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

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null

    try {
      const urlValue = toValue(url)
      const res = await fetch(urlValue)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

使用示例:

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

export default {
  setup() {
    const userId = ref(1)
    const url = computed(() => `/api/users/${userId.value}`)
    const { data, error, loading } = useFetch(url)

    return { data, error, loading, userId }
  }
}

组合函数的命名约定

Script Setup语法糖

<script setup>是组合式API的编译时语法糖,让代码更简洁:

基础用法

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

// 自动暴露给模板
const count = ref(0)
const double = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

使用组件

<script setup>
import MyComponent from './MyComponent.vue'

// 组件自动注册,无需手动声明
</script>

<template>
  <MyComponent />
</template>

defineProps和defineEmits

<script setup>
// 定义props
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// 定义emits
const emit = defineEmits(['update', 'delete'])

function handleUpdate() {
  emit('update', props.count + 1)
}
</script>

<template>
  <div>
    <h3>{{ title }}</h3>
    <p>{{ count }}</p>
    <button @click="handleUpdate">更新</button>
  </div>
</template>

defineExpose

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

const count = ref(0)

function increment() {
  count.value++
}

// 显式暴露给父组件
defineExpose({
  count,
  increment
})
</script>

最佳实践

1. 逻辑组织原则

按功能组织,而非按选项类型:

// ✅ 好的实践
export default {
  setup() {
    // 用户相关逻辑
    const user = ref(null)
    const fetchUser = async () => { /* ... */ }
    
    // 计数器相关逻辑
    const count = ref(0)
    const increment = () => count.value++
    
    // 搜索相关逻辑
    const searchQuery = ref('')
    const searchResults = computed(() => { /* ... */ })
    
    return { user, fetchUser, count, increment, searchQuery, searchResults }
  }
}

2. 提取可复用逻辑

当同一逻辑在多个组件中使用时,提取为组合函数:

// ✅ 好的实践
// composables/useCounter.js
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return { count, increment, decrement, reset }
}

3. 合理使用ref和reactive

// ✅ 推荐:使用ref
const count = ref(0)
const user = ref({ name: 'Vue' })

// ✅ 适用场景:表单对象
const form = reactive({
  username: '',
  email: '',
  password: ''
})

// ❌ 避免:reactive包裹基本类型
const count = reactive(0) // 错误!

// ❌ 避免:解构reactive对象(会丢失响应性)
const { username } = reactive({ username: '' })

4. 使用toRefs保持响应性

import { reactive, toRefs } from 'vue'

function useUser() {
  const state = reactive({
    name: 'Vue',
    age: 3
  })
  
  // 保持响应性
  return toRefs(state)
}

// 使用时
const { name, age } = useUser()

5. 避免过度抽象

// ❌ 过度抽象
function useCount() {
  return ref(0)
}

// ✅ 合理抽象:包含有意义的逻辑
function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  
  return { count, increment, decrement }
}

6. TypeScript集成

import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

export default {
  setup() {
    const user = ref<User | null>(null)
    const userName = computed(() => user.value?.name ?? '未登录')
    
    async function fetchUser(id: number): Promise<void> {
      const response = await fetch(`/api/users/${id}`)
      user.value = await response.json()
    }
    
    return { user, userName, fetchUser }
  }
}

实战案例:待办事项应用

让我们用组合式API构建一个完整的待办事项应用:

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

// 待办事项的类型定义
interface Todo {
  id: number
  text: string
  completed: boolean
}

// 状态管理
const todos = ref<Todo[]>([])
const newTodoText = ref('')
const filter = ref<'all' | 'active' | 'completed'>('all')

// 计算属性
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active':
      return todos.value.filter(t => !t.completed)
    case 'completed':
      return todos.value.filter(t => t.completed)
    default:
      return todos.value
  }
})

const activeCount = computed(() => 
  todos.value.filter(t => !t.completed).length
)

// 方法
function addTodo() {
  if (newTodoText.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: newTodoText.value,
      completed: false
    })
    newTodoText.value = ''
  }
}

function removeTodo(id: number) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) {
    todo.completed = !todo.completed
  }
}

function clearCompleted() {
  todos.value = todos.value.filter(t => !t.completed)
}
</script>

<template>
  <div class="todo-app">
    <h1>待办事项</h1>
    
    <div class="input-section">
      <input
        v-model="newTodoText"
        @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" 
          :checked="todo.completed"
          @change="toggleTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
    
    <div class="footer">
      <span>剩余 {{ activeCount }} 项</span>
      <button 
        v-if="activeCount < todos.length"
        @click="clearCompleted"
      >
        清除已完成
      </button>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.input-section {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.input-section input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.filters button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
}

.filters button.active {
  background: #42b983;
  color: white;
  border-color: #42b983;
}

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

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

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

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #eee;
}
</style>

性能优化技巧

1. 使用shallowRef和shallowReactive

对于大型对象,使用浅层响应式可以提升性能:

import { shallowRef, shallowReactive } from 'vue'

// 只有根级属性是响应式的
const state = shallowReactive({
  nested: {
    count: 0 // 这个不是响应式的
  }
})

// 整体替换才会触发更新
const bigData = shallowRef({
  items: [...] // 大型数组
})

// 触发更新
bigData.value = { items: newItems }

2. 使用computed缓存计算结果

import { ref, computed } from 'vue'

const items = ref([...])

// ✅ 使用computed缓存
const expensiveComputed = computed(() => {
  return items.value.filter(/* 复杂计算 */)
})

// ❌ 避免在方法中重复计算
function expensiveMethod() {
  return items.value.filter(/* 复杂计算 */)
}

3. 合理使用watchEffect

import { ref, watchEffect } from 'vue'

const count = ref(0)

// ✅ watchEffect会自动追踪依赖
watchEffect(() => {
  console.log(count.value)
})

// ❌ 不需要手动声明依赖
watch(() => count.value, (val) => {
  console.log(val)
})

总结

Vue 3的组合式API带来了革命性的变化,它让我们能够:

组合式API并非要取代Options API,而是提供了一种更强大的选择。对于简单组件,Options API依然简洁直观;对于复杂的业务逻辑,组合式API能让代码更加优雅和可维护。

掌握组合式API,就是掌握了Vue 3的精髓。从现在开始,让我们用更优雅的方式编写Vue应用吧!

以上就是深度解析Vue3组合式API的核心概念、使用场景和最佳实践的详细内容,更多关于Vue3组合式API的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文