Vue 3 Composition API 详解

中等 🟡Vue 生态
8 个标签
预计阅读时间:35 分钟
Vue 3Composition APIsetupscript setuprefreactivecomposableTypeScript

Vue 3 Composition API 详解

Composition API 是 Vue 3 引入的一组基于函数的 API,它提供了比 Options API 更灵活的代码组织方式,专门用来解决大型组件"逻辑碎片化"的问题。

如果把 Options API 比作"按抽屉分类"的收纳方式——所有的 data 放一个抽屉、所有的 methods 放一个抽屉、所有的 watch 放一个抽屉;那么 Composition API 就是"按功能打包"的收纳方式——和"搜索功能"相关的响应式数据、计算属性、监听器、方法全部聚在一起。当一个组件长到几百行时,后者的可维护性优势会非常明显。

一、为什么需要 Composition API

在 Vue 2 的 Options API 中,一段完整的业务逻辑往往被强制拆散到不同的选项里:

javascriptCode
// Options API:一个"用户搜索"功能被拆到 4 个地方
export default {
  data() {
    return {
      searchKeyword: '',     // 搜索相关
      searchResult: [],      // 搜索相关
      pageSize: 10,          // 分页相关
      currentPage: 1         // 分页相关
    }
  },
  computed: {
    filteredResult() {       // 搜索相关
      return this.searchResult.filter(item => item.active)
    },
    totalPages() {           // 分页相关
      return Math.ceil(this.searchResult.length / this.pageSize)
    }
  },
  watch: {
    searchKeyword() {        // 搜索相关
      this.doSearch()
    }
  },
  methods: {
    doSearch() { /* 搜索相关 */ },
    changePage() { /* 分页相关 */ }
  }
}

可以看到,"搜索"和"分页"两块逻辑在每个选项里都交织在一起。组件越大,来回滚动定位代码的成本越高。Composition API 允许把同一功能的所有代码写在一起,甚至抽成独立函数复用:

javascriptCode
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    // ===== 搜索逻辑聚在一起 =====
    const searchKeyword = ref('')
    const searchResult = ref([])
    const filteredResult = computed(() =>
      searchResult.value.filter(item => item.active)
    )
    const doSearch = () => { /* ... */ }
    watch(searchKeyword, doSearch)

    // ===== 分页逻辑聚在一起 =====
    const pageSize = ref(10)
    const currentPage = ref(1)
    const totalPages = computed(() =>
      Math.ceil(searchResult.value.length / pageSize.value)
    )
    const changePage = (p) => { currentPage.value = p }

    return {
      searchKeyword, filteredResult,
      currentPage, totalPages, changePage
    }
  }
}

二、setup() 函数

setup()``` 是 Composition API 的入口点,有几个关键特征需要牢记:Code

- 在 ```beforeCreate``` 之前执行,此时组件实例尚未创建,所以 **不能使用 ```this```**。
- 接收两个参数:```props```(响应式的,不能解构,否则丢失响应性)和 ```context```(普通对象,可解构)。
- 返回的对象会暴露给模板、计算属性、生命周期等。

import { ref, toRefs } from 'vue'

export default {

props: {

title: String,

count: Number

},

emits: ['update', 'close'],

setup(props, context) {

// context 包含 attrs / slots / emit / expose,可安全解构

const { emit, attrs, slots, expose } = context

const localCount = ref(props.count)

// 正确:直接访问 props.title 保持响应式

console.log(props.title)

// 错误示范:const { title } = props 会丢失响应性

// 若确实需要解构,使用 toRefs 保持响应式

const { title } = toRefs(props)

const handleClick = () => {

emit('update', localCount.value + 1)

}

return { localCount, title, handleClick }

}

}

codeCode

### 三、`<script setup>` 语法糖
vueCode
<script setup>
import { ref, computed } from 'vue'

// 顶层变量/函数自动暴露给模板,无需 return
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>

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

对比 ```setup()```,同样的逻辑在 ```

codeCode

### 十、真实案例:可复用的分页表格逻辑

下面把前面所有知识点串起来,实现一个真实项目中常见的"带搜索、防抖、分页、加载态"的列表页 composable。

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

import type { Ref } from 'vue'

interface UsePagedListOptions {

fetcher: (params: { page: number; keyword: string }) => Promise<{ list: T[]; total: number }>

pageSize?: number

}

export function usePagedList(options: UsePagedListOptions) {

const { fetcher, pageSize = 10 } = options

const keyword = ref('')

const currentPage = ref(1)

const list = ref([]) as Ref

const total = ref(0)

const loading = ref(false)

const totalPages = computed(() => Math.ceil(total.value / pageSize))

const hasMore = computed(() => currentPage.value < totalPages.value)

let timer: ReturnType | null = null

const load = async () => {

loading.value = true

try {

const res = await fetcher({

page: currentPage.value,

keyword: keyword.value

})

list.value = res.list

total.value = res.total

} finally {

loading.value = false

}

}

// 关键词变化:重置到第一页并防抖加载

watch(keyword, () => {

currentPage.value = 1

if (timer) clearTimeout(timer)

timer = setTimeout(load, 400)

})

// 翻页立即加载

watch(currentPage, load, { immediate: true })

return {

keyword, currentPage, list, total,

loading, totalPages, hasMore,

reload: load

}

}

codeCode

组件中使用只需几行,业务代码高度精简: