【Vue】组合式API(Composition API)

在这里插入图片描述

个人主页:Guiat
归属专栏:Vue

在这里插入图片描述

正文

1. Vue组合式API介绍

组合式API(Composition API)是Vue 3引入的一种新的编写组件逻辑的方式,它是对Vue 2中选项式API(Options API)的补充和扩展。组合式API允许开发者按照逻辑关注点组织代码,提高代码的可读性和可维护性。

1.1 组合式API的核心优势

  • 更好的逻辑复用: 通过可复用的组合函数替代混入(mixins)
  • 更灵活的代码组织: 按功能/逻辑关注点组织代码
  • 更好的类型推断: 为TypeScript提供更自然的支持
  • 更小的包体积: 通过摇树优化(tree-shaking)减小打包体积

1.2 基础概念对比

选项式API (Options API)组合式API (Composition API)
data()ref(), reactive()
methods普通函数
computedcomputed()
watchwatch(), watchEffect()
生命周期钩子onMounted(), onUpdated()等

2. 组合式API核心函数

2.1 setup函数

setup是组合式API的入口点,它在组件实例创建之前执行,此时组件实例尚未被创建。

<script>
export default {
  setup(props, context) {
    // 这里编写使用组合式API的代码
    
    // setup函数需要返回一个对象,对象中的属性会暴露给模板使用
    return {
      // 要暴露给模板的属性和方法
    }
  }
}
</script>

2.2 ref和reactive

2.2.1 ref

ref用于创建一个响应式的数据引用,接受一个任意值作为参数,返回一个带有.value属性的响应式对象。

import { ref } from 'vue'

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

// 更新值
count.value++
console.log(count.value) // 1

在模板中使用时,可以直接使用ref名称,Vue会自动解包.value

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

2.2.2 reactive

reactive用于创建一个响应式对象:

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue',
  users: ['Alice', 'Bob']
})

// 直接修改属性
state.count++
state.name = 'Vue 3'
state.users.push('Charlie')

2.3 computed

computed用于创建计算属性:

import { ref, computed } from 'vue'

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

console.log(doubleCount.value) // 0
count.value = 2
console.log(doubleCount.value) // 4

也可以创建可写的计算属性:

const count = ref(0)
const doubleCount = computed({
  get: () => count.value * 2,
  set: (val) => {
    count.value = val / 2
  }
})

doubleCount.value = 10
console.log(count.value) // 5

3. 生命周期钩子

组合式API提供了一系列生命周期钩子函数,它们可以在setup函数中使用:

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

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

3.1 生命周期对比表

选项式API组合式API
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount()
mountedonMounted()
beforeUpdateonBeforeUpdate()
updatedonUpdated()
beforeUnmountonBeforeUnmount()
unmountedonUnmounted()
errorCapturedonErrorCaptured()
renderTrackedonRenderTracked()
renderTriggeredonRenderTriggered()
activatedonActivated()
deactivatedonDeactivated()

4. watch和watchEffect

4.1 watch

watch用于监听特定的响应式引用或源,并在其变化时执行回调函数:

import { ref, watch } from 'vue'

const count = ref(0)

// 监听单个ref
watch(count, (newValue, oldValue) => {
  console.log(`count从${oldValue}变为${newValue}`)
})

// 监听多个源
const name = ref('Vue')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})

// 监听对象的深层属性
const user = reactive({
  name: 'Alice',
  age: 25
})
watch(() => user.name, (newName, oldName) => {
  console.log(`name从${oldName}变为${newName}`)
})

// 深度监听
watch(user, (newUser, oldUser) => {
  console.log('user对象发生变化')
}, { deep: true })

4.2 watchEffect

watchEffect会立即执行一次回调函数,并自动追踪其中的响应式依赖,当这些依赖变化时重新执行回调:

import { ref, watchEffect } from 'vue'

const count = ref(0)
const name = ref('Vue')

watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`)
})
// 立即输出: "count: 0, name: Vue"

// 修改值后会触发watchEffect重新执行
count.value++
// 输出: "count: 1, name: Vue"

4.3 清理副作用

watchwatchEffect都支持在回调函数中返回一个清理函数,用于在下次执行前清理副作用:

watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('执行某些操作')
  }, 1000)
  
  // 在watchEffect重新执行前或停止时调用
  onCleanup(() => {
    clearTimeout(timer)
  })
})

5. 组合函数(Composables)

组合函数是组合式API最强大的特性之一,它允许你封装和复用有状态的逻辑:

5.1 创建组合函数

组合函数通常以use开头,返回一个包含所需状态和方法的对象:

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

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

5.2 使用组合函数

在组件中使用组合函数:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

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

export default {
  setup() {
    // 解构并使用组合函数返回的内容
    const { count, doubled, increment, decrement } = useCounter(10)
    
    return {
      count,
      doubled,
      increment,
      decrement
    }
  }
}
</script>

6. script setup语法糖

Vue 3.2引入了<script setup>语法糖,进一步简化组合式API的使用:

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

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

// 在<script setup>中的变量和函数会自动暴露给模板
const count = ref(0)

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

6.1 特点和优势

  • 更简洁的语法,无需返回对象
  • 自动将变量暴露给模板
  • 更好的IDE支持和类型推断
  • 组件和指令可以直接使用,无需注册

6.2 defineProps和defineEmits

<script setup>中使用props和emits:

<script setup>
// 声明props
const props = defineProps({
  message: String,
  count: {
    type: Number,
    default: 0
  }
})

// 声明emits
const emit = defineEmits(['increment', 'update'])

function handleClick() {
  emit('increment', 1)
}
</script>

6.3 defineExpose

默认情况下,<script setup>组件是关闭的(父组件无法访问子组件的属性和方法)。使用defineExpose显式暴露内部属性:

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

const count = ref(0)
function increment() { count.value++ }

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

7. 组合式API实战案例

7.1 实现一个Todo List

<template>
  <div class="todo-app">
    <h1>Todo List</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">
        <input 
          type="checkbox" 
          :checked="todo.completed" 
          @change="toggleTodo(todo.id)" 
        />
        <span :class="{ completed: todo.completed }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
    
    <div class="todo-stats" v-if="todos.length > 0">
      <span>{{ activeCount }} 个待办项</span>
      <button v-if="completedCount > 0" @click="clearCompleted">
        清除已完成
      </button>
    </div>
  </div>
</template>

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

// 状态
const newTodo = ref('')
const todos = ref([
  { id: 1, text: '学习Vue 3', completed: false },
  { id: 2, text: '掌握组合式API', completed: false },
  { id: 3, text: '构建项目', completed: false }
])
const filter = ref('all')
let nextId = 4

// 计算属性
const filteredTodos = computed(() => {
  if (filter.value === 'active') {
    return todos.value.filter(todo => !todo.completed)
  } else if (filter.value === 'completed') {
    return todos.value.filter(todo => todo.completed)
  } else {
    return todos.value
  }
})

const activeCount = computed(() => {
  return todos.value.filter(todo => !todo.completed).length
})

const completedCount = computed(() => {
  return todos.value.filter(todo => todo.completed).length
})

// 方法
function addTodo() {
  const text = newTodo.value.trim()
  if (text) {
    todos.value.push({
      id: nextId++,
      text,
      completed: false
    })
    newTodo.value = ''
  }
}

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

function removeTodo(id) {
  const index = todos.value.findIndex(todo => todo.id === id)
  if (index !== -1) {
    todos.value.splice(index, 1)
  }
}

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

<style scoped>
.todo-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  text-align: center;
}

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

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

.add-todo button {
  padding: 8px 15px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
}

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

.filters button {
  margin: 0 5px;
  padding: 5px 10px;
  border: 1px solid #ddd;
  background-color: white;
  cursor: pointer;
  border-radius: 4px;
}

.filters button.active {
  background-color: #ddd;
}

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

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

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

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

.todo-list button {
  padding: 5px 8px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-stats {
  display: flex;
  justify-content: space-between;
  margin-top: 15px;
  color: #666;
}

.todo-stats button {
  padding: 5px 10px;
  background-color: #ddd;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

7.2 封装一个useAxios组合函数

// useAxios.js
import { ref, watch } from 'vue'
import axios from 'axios'

export function useAxios(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  const response = ref(null)
  
  // 可配置参数
  const config = {
    immediate: true, // 是否立即执行请求
    ...options
  }
  
  // 执行请求的函数
  const execute = async (customUrl = url, customOptions = {}) => {
    loading.value = true
    error.value = null
    data.value = null
    
    try {
      const result = await axios({
        url: customUrl,
        ...config,
        ...customOptions
      })
      
      response.value = result
      data.value = result.data
      return result
    } catch (err) {
      error.value = err
      return Promise.reject(err)
    } finally {
      loading.value = false
    }
  }
  
  // 如果设置了immediate,则立即执行请求
  if (config.immediate && url) {
    execute()
  }
  
  // 取消请求
  const cancel = () => {
    if (axios.isCancel) {
      // 实现取消请求的逻辑
    }
  }
  
  // 刷新请求
  const refresh = () => {
    return execute()
  }
  
  return {
    data,
    loading,
    error,
    response,
    execute,
    cancel,
    refresh
  }
}

// 使用示例
/*
import { useAxios } from './useAxios'

export default {
  setup() {
    const { 
      data, 
      loading, 
      error, 
      execute 
    } = useAxios('https://jsonplaceholder.typicode.com/posts')
    
    return {
      data,
      loading,
      error,
      fetchData: execute
    }
  }
}
*/

8. 组合式API与TypeScript

组合式API为TypeScript提供了出色的类型支持:

8.1 基本类型定义

import { ref, reactive, computed } from 'vue'

// 为ref指定类型
const count = ref<number>(0)

// 为reactive指定类型
interface User {
  name: string
  age: number
}
const user = reactive<User>({
  name: 'Vue',
  age: 3
})

// 为computed指定类型
const doubleCount = computed<number>(() => count.value * 2)

8.2 组件Props类型定义

<script setup lang="ts">
// 使用接口定义Props
interface Props {
  message: string
  count?: number
  items: string[]
  callback?: (id: number) => void
}

// 声明具有类型的props
const props = defineProps<Props>()

// 为props提供默认值
const defaults = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => ['默认项']
})
</script>

8.3 事件类型定义

<script setup lang="ts">
// 定义发出的事件类型
const emit = defineEmits<{
  (e: 'update', id: number): void
  (e: 'delete', id: number): void
}>()

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

9. 性能优化最佳实践

9.1 使用shallowRef和shallowReactive

当处理大型数据结构且只需要跟踪顶层属性的变化时,使用浅层响应式可以提高性能:

import { shallowRef, shallowReactive } from 'vue'

// 只有.value改变时才会触发更新,不会深度追踪对象内部属性
const state = shallowRef({ count: 0, deep: { nested: 'value' } })

// 只跟踪顶层属性的变化
const user = shallowReactive({
  name: 'Vue',
  profile: { age: 3 }
})

9.2 使用toRaw获取原始对象

当需要处理大量数据但不需要触发响应式更新时,可以使用toRaw获取原始对象:

import { reactive, toRaw } from 'vue'

const state = reactive({ count: 0, items: [] })

// 获取原始对象,对它的修改不会触发视图更新
const rawState = toRaw(state)

9.3 使用markRaw标记永不需要转换的对象

对于那些永远不需要转换为响应式的对象,使用markRaw可以避免不必要的性能开销:

import { reactive, markRaw } from 'vue'

// 不会被转换为响应式
const data = markRaw({
  large: 'data structure',
  with: 'many properties'
})

const state = reactive({
  title: 'My App',
  // data将保持非响应式
  data
})

10. 组合式API常见问题与解决方案

10.1 响应式丢失问题

问题:解构reactive对象会导致响应式丢失

const state = reactive({ count: 0 })
// 错误用法:响应式丢失
const { count } = state 

解决方法

// 方法1:使用toRefs保持响应性
import { toRefs } from 'vue'
const { count } = toRefs(state)

// 方法2:使用计算属性
import { computed } from 'vue'
const count = computed({
  get: () => state.count,
  set: (val) => state.count = val
})

// 方法3:直接使用state.count

10.2 ref的自动解包规则

在组合式API中,ref的自动解包有一些特殊情况需要注意:

  1. 在模板中会自动解包
  2. 作为reactive对象的属性会自动解包
  3. 在数组或普通对象中不会自动解包
const count = ref(0)
const state = reactive({
  count, // 在reactive中会自动解包
  users: [ref('Alice')] // 在数组中不会自动解包
})

console.log(state.count) // 直接访问值,不需要.value
console.log(state.users[0].value) // 需要使用.value

10.3 避免过度使用watchEffect

watchEffect虽然强大,但可能会导致不必要的重复执行。应该谨慎使用,并考虑以下优化:

// 优化前
watchEffect(() => {
  console.log(`name: ${user.name}, age: ${user.age}`)
  // 只关心name的变化,但age变化也会触发
})

// 优化后
watch(() => user.name, (newName) => {
  console.log(`name: ${newName}, age: ${user.age}`)
}, { immediate: true })

11. 高级用法

11.1 provide和inject

组合式API中的依赖注入:

// 父组件
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)
    
    return { theme, toggleTheme }
  }
}

// 子/孙组件
import { inject } from 'vue'

export default {
  setup() {
    // 注入父/祖先组件提供的值
    const theme = inject('theme')
    const toggleTheme = inject('toggleTheme')
    
    return { theme, toggleTheme }
  }
}

11.2 自定义ref

创建自定义的响应式引用:

import { customRef } from 'vue'

// 创建一个防抖ref
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // 追踪依赖
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // 触发更新
        }, delay)
      }
    }
  })
}

// 使用
const searchQuery = useDebouncedRef('', 500)

11.3 Suspense和异步组件

组合式API中处理异步组件:

<!-- 父组件使用Suspense -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<!-- 异步组件 -->
<script setup>
import { ref } from 'vue'

// setup可以是异步的
const data = await fetch('/api/data').then(r => r.json())
const processedData = ref(data)
</script>

12. 从选项式API迁移到组合式API

12.1 渐进式迁移策略

  1. 不需要一次性全部迁移
  2. 可以在同一个组件中混合使用两种API
<script>
import { ref, computed, onMounted } from 'vue'

export default {
  // 选项式API
  props: ['title'],
  components: { /* ... */ },
  
  // 组合式API
  setup(props) {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    return {
      count,
      doubleCount
    }
  },
  
  // 依然可以使用选项式API的选项
  data() {
    return {
      message: 'Hello'
    }
  },
  methods: {
    // ...
  }
}
</script>

12.2 常见模式转换

12.2.1 data → ref/reactive

// 选项式API
data() {
  return {
    count: 0,
    user: { name: 'Vue', age: 3 }
  }
}

// 组合式API
setup() {
  const count = ref(0)
  const user = reactive({ name: 'Vue', age: 3 })
  
  return { count, user }
}

12.2.2 methods → 普通函数

// 选项式API
methods: {
  increment() {
    this.count++
  }
}

// 组合式API
setup() {
  const count = ref(0)
  
  function increment() {
    count.value++
  }
  
  return { count, increment }
}

12.2.3 computed → computed()

// 选项式API
computed: {
  doubleCount() {
    return this.count * 2
  }
}

// 组合式API
setup() {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  
  return { count, doubleCount }
}

13. 结合第三方库

13.1 Vue Router与组合式API

使用组合式API的Vue Router钩子:

import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()
    
    // 使用路由参数
    console.log(route.params.id)
    
    // 导航
    function navigateTo(id) {
      router.push(`/user/${id}`)
    }
    
    return { navigateTo }
  }
}

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Guiat

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

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

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

打赏作者

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

抵扣说明:

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

余额充值