组合式 API(Composition API)
Vue 3 中的组合式 API 是一种新的方式,用来组织和复用 Vue 组件中的逻辑。与 Vue 2 中的 Options API(通过 data, methods, computed, watch 等选项来定义组件)不同,组合式 API 使用函数来声明组件的逻辑和状态,使得代码更具模块化和可维护性。
核心理念
组合式 API 主要通过函数的方式,将组件的逻辑组织在一起。其主要目标是:
- 提高代码的可读性和可维护性: 特别是对于复杂组件,通过将逻辑分割成更小的函数,可以使代码更易于理解和维护。
- 增强逻辑复用性: 可以将逻辑提取到独立的函数或文件中,以便在多个组件间共享。
主要组成部分
1. setup()函数
setup()
函数是在组件中使用组合式 API 的入口,它是在组件实例创建之前调用的,用于初始化组件的逻辑。通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
注意
对于结合单文件组件 (SFC) 使用的组合式 API,推荐通过 <script setup>
以获得更加简洁及符合人体工程学的语法。 <script setup>
是在单文件组件中使用组合式 API 的编译时语法糖。
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment,
}
},
}
setup
函数的第一个参数是组件的 props
,一个 setup
函数的 props
是响应式的,并且会在传入新的 props 时同步更新。
setup
函数的第二个参数是一个Setup 上下文对象 context
,它包含了以下属性:
- attrs:透传 Attributes(非响应式的对象,等价于 $attrs)
- slots:插槽(非响应式的对象,等价于 $slots)
- emit:触发事件(函数,等价于 $emit)
- expose:暴露公共属性(函数)
setup
函数返回的对象会暴露给模板和组件实例。
2. 响应式 API
ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
const count = ref(0)
count.value++ // 修改值需要通过 .value 属性
reactive()
用于创建一个响应式对象。
const state = reactive({
count: 0,
})
state.count++ // 直接修改对象属性
computed()
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
/**
* 计算属性的 onTrack 和 onTrigger 选项仅会在开发模式下工作。
* onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。
* onTrigger 将在侦听器回调被依赖项的变更触发时被调用。
*/
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// 当 count.value 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 count.value 被更改时触发
debugger
},
})
// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)
// 更改 count.value,应该会触发 onTrigger
count.value++
watch()
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。flush
:调整回调函数的刷新时机。onTrack / onTrigger
:调试侦听器的依赖。once
:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
/**
* 当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值
*/
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
watch(source, callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
},
})
当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果想让回调在深层级变更时也能触发,需要使用 { deep: true }
强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。
const state = reactive({ count: 0 })
watch(
() => state,
(newValue, oldValue) => {
// newValue === oldValue
},
{ deep: true }
)
当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
const state = reactive({ count: 0 })
watch(state, () => {
/* 深层级变更状态所触发的回调 */
})
3. 生命周期钩子
- onMounted(): 注册一个回调函数,在组件挂载完成后执行
- onUpdated(): 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
- onUnmounted(): 注册一个回调函数,在组件实例被卸载之后调用。
- onBeforeMount(): 注册一个钩子,在组件被挂载之前被调用。
- onBeforeUpdate(): 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
- onBeforeUnmount(): 注册一个钩子,在组件实例被卸载之前调用。
- 。。。
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('Component mounted')
})
onUnmounted(() => {
console.log('Component unmounted')
})
</script>
4. 依赖注入
provide()
提供一个值,可以被后代组件注入。
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
<script setup>
import { ref, provide } from 'vue'
import { countSymbol } from './injectionSymbols'
// 提供静态值
provide('path', '/project/')
// 提供响应式的值
const count = ref(0)
provide('count', count)
// 提供时将 Symbol 作为 key
provide(countSymbol, count)
</script>
inject()
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject()
将返回 undefined
,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。
第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,必须将 true
作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。
<script setup>
import { inject } from 'vue'
import { countSymbol } from './injectionSymbols'
// 注入不含默认值的静态值
const path = inject('path')
// 注入响应式的值
const count = inject('count')
// 通过 Symbol 类型的 key 注入
const count2 = inject(countSymbol)
// 注入一个值,若为空则使用提供的默认值
const bar = inject('path', '/default-path')
// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('factory', () => new ExpensiveObject(), true)
</script>
5. 自定义 Hook
自定义 Hook(也称为组合函数)允许你将逻辑提取到独立的函数中,以便在多个组件中复用。
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
return { count, increment }
}
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>
<template>
count: {{ count }}
<button @click="increment">increment</button>
</template>
count: 0
与 Options API 的对比
- 逻辑组织: Composition API 将组件的逻辑更清晰地组织成函数,而不是分散在多个选项(如 data、methods、computed 等)中。
- 逻辑复用: 可以更容易地提取和复用逻辑。
- 类型支持: 对于 TypeScript 用户,Composition API 提供了更好的类型推导和支持。