【Vue3组合式API实战指南:告别Options API的烦恼】

Vue3的Composition API彻底改变了Vue的开发方式,本文将深入剖析组合式API的核心概念和最佳实践,帮助你快速掌握Vue3开发。

一、为什么需要Composition API?

1.1 Options API的痛点

痛点表现:

  • 逻辑分散:相关代码被data、methods、computed等选项分割
  • 代码复用困难:mixins容易命名冲突,来源不清晰
  • 类型推导弱:TypeScript支持不够友好
  • 大组件难维护:代码量大时难以理解和维护
javascript 复制代码
// ❌ Options API:逻辑分散
export default {
  data() {
    return {
      count: 0,
      user: null,
      loading: false
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    async fetchUser() {
      this.loading = true
      this.user = await api.getUser()
      this.loading = false
    }
  },
  mounted() {
    this.fetchUser()
  }
}

1.2 Composition API的优势

  • ✅ 逻辑聚合:相关代码组织在一起
  • ✅ 代码复用:通过组合函数轻松复用
  • ✅ 类型推导:完美支持TypeScript
  • ✅ Tree-shaking:未使用的代码可以被移除

二、核心API详解

2.1 ref 和 reactive

关键点: ref用于基本类型,reactive用于对象类型。

vue 复制代码
<template>
  <div>
    <!-- ref需要.value访问,模板中自动解包 -->
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    
    <!-- reactive对象直接访问属性 -->
    <p>用户: {{ user.name }}</p>
    <p>年龄: {{ user.age }}</p>
    
    <button @click="increment">增加</button>
    <button @click="updateUser">更新用户</button>
  </div>
</template>

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

// ✅ ref:基本类型响应式
const count = ref(0)
const message = ref('Hello')

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

// ✅ reactive:对象类型响应式
const user = reactive({
  name: '张三',
  age: 25,
  address: {
    city: '北京'
  }
})

// 直接访问属性
console.log(user.name) // 张三
user.age = 26

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

// 方法
const increment = () => {
  count.value++
}

const updateUser = () => {
  user.name = '李四'
  user.age = 30
}

// ❌ 常见错误:解构reactive会失去响应式
const { name, age } = user // 失去响应式!

// ✅ 正确:使用toRefs保持响应式
import { toRefs } from 'vue'
const { name, age } = toRefs(user) // 保持响应式
</script>

痛点解决: 清晰的响应式数据管理,避免this指向问题。


2.2 生命周期钩子

关键点: 组合式API中的生命周期钩子以on开头。

vue 复制代码
<script setup>
import { 
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue'

// 组件挂载前
onBeforeMount(() => {
  console.log('组件即将挂载')
})

// 组件挂载后(最常用)
onMounted(() => {
  console.log('组件已挂载')
  // 适合:DOM操作、发起请求、初始化第三方库
  fetchData()
  initChart()
})

// 组件更新前
onBeforeUpdate(() => {
  console.log('组件即将更新')
})

// 组件更新后
onUpdated(() => {
  console.log('组件已更新')
  // 注意:避免在这里修改状态,可能导致无限循环
})

// 组件卸载前
onBeforeUnmount(() => {
  console.log('组件即将卸载')
  // 适合:清理定时器、取消请求、移除事件监听
})

// 组件卸载后
onUnmounted(() => {
  console.log('组件已卸载')
})

// 错误捕获
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err)
  return false // 阻止错误继续传播
})

// ✅ 可以多次调用同一个钩子
onMounted(() => {
  console.log('第一个mounted')
})

onMounted(() => {
  console.log('第二个mounted')
})
</script>

对比Options API:

Options API Composition API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

2.3 watch 和 watchEffect

关键点: watch需要明确指定监听源,watchEffect自动追踪依赖。

vue 复制代码
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三', age: 25 })

// ✅ watch:监听单个ref
watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变为${newVal}`)
})

// ✅ watch:监听多个源
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
  console.log('count或age变化了')
})

// ✅ watch:监听reactive对象(深度监听)
watch(user, (newVal, oldVal) => {
  console.log('user对象变化了')
  // 注意:newVal和oldVal是同一个对象引用
}, { deep: true })

// ✅ watch:监听reactive对象的某个属性
watch(() => user.name, (newName, oldName) => {
  console.log(`name从${oldName}变为${newName}`)
})

// ✅ watchEffect:自动追踪依赖
watchEffect(() => {
  // 自动追踪count和user.age的变化
  console.log(`count: ${count.value}, age: ${user.age}`)
})

// watch选项
watch(count, (newVal) => {
  console.log(newVal)
}, {
  immediate: true, // 立即执行一次
  deep: true,      // 深度监听
  flush: 'post'    // 在DOM更新后执行
})

// 停止监听
const stop = watch(count, (newVal) => {
  console.log(newVal)
  if (newVal > 10) {
    stop() // 停止监听
  }
})

// ❌ 常见错误:监听reactive对象的属性
watch(user.name, (newVal) => { // 错误!
  console.log(newVal)
})

// ✅ 正确写法
watch(() => user.name, (newVal) => {
  console.log(newVal)
})
</script>

痛点解决: 灵活的数据监听,避免不必要的性能开销。


2.4 computed 计算属性

关键点: computed具有缓存特性,只有依赖变化时才重新计算。

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// ✅ 只读计算属性
const fullName = computed(() => {
  console.log('计算fullName') // 只在依赖变化时执行
  return `${firstName.value} ${lastName.value}`
})

// ✅ 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    const names = value.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

// 使用
console.log(fullName.value) // 张 三
fullNameWritable.value = '李 四' // 触发set
console.log(firstName.value) // 李
console.log(lastName.value) // 四

// ❌ 常见错误:在computed中修改依赖
const badComputed = computed(() => {
  firstName.value = '王' // 错误!不要在computed中修改依赖
  return firstName.value
})

// ✅ computed vs method
const list = ref([1, 2, 3, 4, 5])

// computed:有缓存,依赖不变不重新计算
const filteredList = computed(() => {
  console.log('计算filteredList')
  return list.value.filter(n => n > 2)
})

// method:无缓存,每次调用都执行
const getFilteredList = () => {
  console.log('执行getFilteredList')
  return list.value.filter(n => n > 2)
}
</script>

<template>
  <div>
    <!-- computed:多次访问只计算一次 -->
    <p>{{ filteredList }}</p>
    <p>{{ filteredList }}</p>
    
    <!-- method:每次都执行 -->
    <p>{{ getFilteredList() }}</p>
    <p>{{ getFilteredList() }}</p>
  </div>
</template>

痛点解决: 自动缓存计算结果,避免重复计算,提升性能。


三、组合函数(Composables)

3.1 基础组合函数

关键点: 将可复用的逻辑提取为组合函数,实现代码复用。

javascript 复制代码
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const doubleCount = computed(() => count.value * 2)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}
vue 复制代码
<!-- 使用组合函数 -->
<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="reset">重置</button>
  </div>
</template>

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

// 可以创建多个独立的计数器
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
const counter2 = useCounter(0)
</script>

3.2 实战:鼠标位置追踪

javascript 复制代码
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  const update = (event) => {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}
vue 复制代码
<template>
  <div>
    <p>鼠标位置: {{ x }}, {{ y }}</p>
  </div>
</template>

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

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

3.3 实战:异步数据获取

javascript 复制代码
// 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)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    data.value = null
    
    try {
      // toValue可以处理ref和普通值
      const urlValue = toValue(url)
      const response = await fetch(urlValue)
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      data.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
  
  // 当url变化时重新获取
  watchEffect(() => {
    fetchData()
  })
  
  const refetch = () => {
    fetchData()
  }
  
  return { data, error, loading, refetch }
}
vue 复制代码
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error }}</div>
    <div v-else-if="data">
      <h3>{{ data.title }}</h3>
      <p>{{ data.body }}</p>
    </div>
    <button @click="refetch">刷新</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://jsonplaceholder.typicode.com/posts/${userId.value}`)

const { data, error, loading, refetch } = useFetch(url)
</script>

3.4 实战:本地存储

javascript 复制代码
// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  // 从localStorage读取初始值
  const storedValue = localStorage.getItem(key)
  const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
  
  // 监听变化并同步到localStorage
  watch(data, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  // 清除
  const remove = () => {
    localStorage.removeItem(key)
    data.value = defaultValue
  }
  
  return { data, remove }
}
vue 复制代码
<template>
  <div>
    <input v-model="username.data" placeholder="输入用户名">
    <p>保存的用户名: {{ username.data }}</p>
    <button @click="username.remove">清除</button>
  </div>
</template>

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

const username = useLocalStorage('username', '')
</script>

痛点解决: 逻辑复用变得简单,代码更加模块化和可维护。


四、高级技巧

4.1 provide / inject 依赖注入

关键点: 跨层级组件通信,避免props层层传递。

vue 复制代码
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent />
  </div>
</template>

<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const user = ref({ name: '张三', role: 'admin' })

// 提供数据
provide('theme', theme)
provide('user', readonly(user)) // 只读,防止子组件修改

// 提供方法
const updateTheme = (newTheme) => {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>
vue 复制代码
<!-- 子组件(任意层级) -->
<template>
  <div :class="theme">
    <p>当前主题: {{ theme }}</p>
    <p>用户: {{ user.name }}</p>
    <button @click="updateTheme('light')">切换主题</button>
  </div>
</template>

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

// 注入数据
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')

// 提供默认值
const config = inject('config', { timeout: 3000 })
</script>

4.2 defineExpose 暴露组件方法

关键点: <script setup>默认是封闭的,需要显式暴露给父组件。

vue 复制代码
<!-- 子组件 -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

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

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

const updateMessage = (newMessage) => {
  message.value = newMessage
}

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

// 暴露给父组件
defineExpose({
  message,
  count,
  updateMessage,
  increment
})
</script>
vue 复制代码
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent ref="childRef" />
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const childRef = ref(null)

const callChildMethod = () => {
  // 访问子组件暴露的属性和方法
  console.log(childRef.value.message)
  childRef.value.updateMessage('New Message')
  childRef.value.increment()
}
</script>

4.3 自定义指令

javascript 复制代码
// directives/vFocus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// directives/vClickOutside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}
vue 复制代码
<template>
  <div>
    <!-- 自动聚焦 -->
    <input v-focus placeholder="自动聚焦">
    
    <!-- 点击外部关闭 -->
    <div v-click-outside="closeDropdown" class="dropdown">
      <button @click="showDropdown = !showDropdown">下拉菜单</button>
      <ul v-if="showDropdown">
        <li>选项1</li>
        <li>选项2</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { vFocus, vClickOutside } from '@/directives'

const showDropdown = ref(false)

const closeDropdown = () => {
  showDropdown.value = false
}
</script>

五、实战案例:Todo应用

vue 复制代码
<template>
  <div class="todo-app">
    <h1>待办事项</h1>
    
    <!-- 输入框 -->
    <div class="input-box">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        placeholder="添加新任务..."
      >
      <button @click="addTodo">添加</button>
    </div>
    
    <!-- 过滤器 -->
    <div class="filters">
      <button
        v-for="filter in filters"
        :key="filter"
        :class="{ active: currentFilter === filter }"
        @click="currentFilter = filter"
      >
        {{ filter }}
      </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.id)">删除</button>
      </li>
    </ul>
    
    <!-- 统计 -->
    <div class="stats">
      <span>总计: {{ todos.length }}</span>
      <span>已完成: {{ completedCount }}</span>
      <span>未完成: {{ activeCount }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useLocalStorage } from '@/composables/useLocalStorage'

// 使用本地存储
const { data: todos } = useLocalStorage('todos', [])
const newTodo = ref('')
const currentFilter = ref('全部')
const filters = ['全部', '未完成', '已完成']

// 添加任务
const addTodo = () => {
  if (!newTodo.value.trim()) return
  
  todos.value.push({
    id: Date.now(),
    text: newTodo.value,
    completed: false
  })
  
  newTodo.value = ''
}

// 删除任务
const removeTodo = (id) => {
  const index = todos.value.findIndex(todo => todo.id === id)
  if (index > -1) {
    todos.value.splice(index, 1)
  }
}

// 过滤任务
const filteredTodos = computed(() => {
  switch (currentFilter.value) {
    case '未完成':
      return todos.value.filter(todo => !todo.completed)
    case '已完成':
      return todos.value.filter(todo => todo.completed)
    default:
      return todos.value
  }
})

// 统计
const completedCount = computed(() => 
  todos.value.filter(todo => todo.completed).length
)

const activeCount = computed(() => 
  todos.value.filter(todo => !todo.completed).length
)
</script>

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

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

.input-box 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;
  cursor: pointer;
  border-radius: 4px;
}

.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;
}

.stats {
  display: flex;
  justify-content: space-around;
  margin-top: 20px;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
}
</style>

六、最佳实践

✅ 命名规范

  • 组合函数以use开头:useCounteruseMouse
  • 事件处理函数以handleon开头:handleClickonSubmit
  • 布尔值以ishasshould开头:isLoadinghasError

✅ 代码组织

javascript 复制代码
// 推荐的代码组织顺序
<script setup>
// 1. 导入
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'

// 2. 组合函数
const router = useRouter()
const { data, loading } = useFetch('/api/data')

// 3. 响应式数据
const count = ref(0)
const user = reactive({ name: '' })

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

// 5. 监听器
watch(count, (newVal) => {
  console.log(newVal)
})

// 6. 生命周期
onMounted(() => {
  fetchData()
})

// 7. 方法
const increment = () => {
  count.value++
}

// 8. 暴露
defineExpose({ increment })
</script>

✅ 性能优化

  • 使用shallowRefshallowReactive处理大对象
  • 使用markRaw标记不需要响应式的对象
  • 合理使用computed缓存计算结果
  • 避免在模板中使用复杂表达式

七、总结

Vue3 Composition API的核心优势:

  1. 逻辑复用 - 通过组合函数轻松复用逻辑
  2. 代码组织 - 相关代码聚合在一起,易于维护
  3. 类型推导 - 完美支持TypeScript
  4. 性能优化 - 更好的Tree-shaking支持

记住:Composition API不是替代Options API,而是提供了更灵活的选择


相关资源


💡 小贴士: 从小项目开始尝试Composition API,逐步掌握其精髓!

关注我,获取更多Vue干货! 🚀

相关推荐
一勺-_-2 小时前
mermaid图片如何保存成svg格式
开发语言·javascript·ecmascript
否子戈2 小时前
WebCut前端视频编辑UI框架一周开源进度
前端·音视频开发·ui kit
昔人'2 小时前
`corepack` 安装pnpm
前端·pnpm·node·corepack
萌萌哒草头将军3 小时前
pnpm + monorepo 才是 AI 协同开发的最佳方案!🚀🚀🚀
前端·react.js·ai编程
hboot3 小时前
💪别再迷茫!一份让你彻底掌控 TypeScript 类型系统的终极指南
前端·typescript
GISer_Jing4 小时前
深入拆解Taro框架多端适配原理
前端·javascript·taro
毕设源码-邱学长4 小时前
【开题答辩全过程】以 基于VUE的藏品管理系统的设计与实现为例,包含答辩的问题和答案
前端·javascript·vue.js
San30.4 小时前
深入理解 JavaScript:手写 `instanceof` 及其背后的原型链原理
开发语言·javascript·ecmascript
北冥有一鲲4 小时前
LangChain.js:RAG 深度解析与全栈实践
开发语言·javascript·langchain