Vue 3 组合式 API

vue3

Vue 3 组合式 API

Vue 通过组件可以实现代码重用,但是由于组件基于各种选项,如 datacomputedmethodswatch 等,使得同一个功能的逻辑分离四散,所以在处理单个逻辑关注点时,不得不在相关代码的选项块之间不断「跳转」,这种碎片化使得理解和维护复杂组件变得十分困难。

因此 Vue 3 提供了组合式 API,即各种组件选项的相应替代方法,它们在选项 setup()<script setup> 中使用,可以让单个功能的逻辑代码集合到一起,便于管理维护,还可以实现更精细的代码共享和重用。

setup

Vue 3 新增了一个组件选项 setup,它是一个函数,在其中可以调用组合式 API,其返回的所有内容都暴露给组件的其余部分(其他选项,如 computedmethods、生命周期钩子等等),一般是供模板使用的

  • 一些响应式变量,相当于 data 选项的作用
  • 一些函数,相当于 methods 选项的作用
js
export default {
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    console.log(props) // { user: '' }

    return {} // 这里返回的任何内容都可以用于组件的其余部分
  }
}
提示

虽然 Vue 支持混合使用选项 API 和组合式 API,但不推荐在同一个组件中这样用

setup 函数是在组件创建之前(在 datacomputedmethods 选项被解析之前,可以将其作用视为 created 钩子函数,实际比 beforeCreate 更早)执行,一旦 props 被解析,就将作为组合式 API 的入口。

因此在 setup() 中应该避免使用 this(因为它找不到组件实例)和访问 datacomputedmethods 选项。

setup(props, context) 接收两个参数:

  • props
  • context

props

props 一个对象,包含从父级传递进来的响应式数据,因此当传入的 prop 更新时,它将被更新。

所有在子组件中声明了的 prop,不管父组件是否向其传递了,都将出现在 props 对象中。其中未被传入的可选的 prop 的值会是 undefined

提示

props 对象中的 property 可以在模板中直接访问

注意

因为 props 对象是响应式的,所以不应该使用 ES6 解构语法去读取其中的属性,因为这可能会导致变量的响应性丢失(如果解构后,变量「接到」的值是基础数据类型,那么变量就会丢失响应性)。

如果真的需要解构 props,可先使用函数 toRefs(props)props 对象进行封装处理,这样 props 对象的每个属性(即每个 prop)会被包裹为一个 ref 对象,这样 props 对象解构后分配给每个变量都指向源对象的属性,即依然保持了数据的响应性

如果一个 prop 是可选的,例如 title 是可选的 prop,则调用组件时,可能传入的 props 中没有 title,在这种情况下,toRefs 处理 props 对象时,将不会为 title 创建一个 ref 对象,此时需要使用 toRef(props, title) 来替代,专门手动/显式地为该属性创建一个 ref 对象作为属性值。

context

context 一个对象,不是响应式的,可以通过解构来访问组件的三个 property

  • attrs 是一个对象,包含了组件所有非 props 的属性
  • slots 是一个对象,包含组件通过插槽分发的内容
  • emit 是一个方法,可以通过调用该方法,手动触发一个自定义事件
提示

由于 attrsslots非响应式的,如果打算根据 attrsslots 更改应用副作用(响应它们的变化执行相应的操作),那么应该在 onUpdated 生命周期钩子中访问它们并执行相应的操作。

但是由于 attrsslots 是有状态的对象,它们是会随组件本身的更新而更新,因此应该避免对它们本身进行解构,并始终以 attrs.xslots.x 的方式引用它们的属性

响应性

在选项式 API data 中定义的属性都可以成为响应式数据,而在组合式 API 中 Vue 3 提供了多个函数对数据进行处理,使得存储该数据的变量具有响应性。

Tip

更多关于响应式 API 使用技巧可以参考这个视频和这一篇文章

添加响应式

Vue 3 提供 ref()reactive() 两个函数来手动为数据添加响应性。

对于基础数据类型的值推荐使用 ref 函数获取响应性变量;而对于对象这一类原来就是引用数据类型的值推荐使用 reactive 函数获取响应性变量。

提示

虽然 ref 函数也可接收对象数据类型的值,但是由于对象已经是通过引用传递的,因此无需再使用 ref 函数为其包裹一层 ref 对象。所以对于对象推荐使用 reactive 函数获取响应性变量。

ref

ref 函数可以接收任何类型的数据,然后它会返回一个对象,称为 ref 对象。

该对象只有一个属性 value,它将接收到的参数作为该属性的初始值,即将传入的值「包裹」在一个对象中,为该值创建了一个响应式引用(存储 ref 对象的变量具有响应式,即更改其值后,模板也会相应更新)。由于通过封装,因此在读取或更改数据,需要访问变量的属性 value

提示

setup 选项最后 return 返回的对象中 Vue 会判断变量是否为 ref 对象,如果是就会自动浅解包 unwrap,因此在模板或其他选项中不需要通过属性 value 读取变量的值,直接使用变量即可(但是如果要访问 ref 对象中嵌套的值时,仍需要在模板中添加 .value,例如 nested.count.value

vue
<template>
  <div>
    <p>{{ counter }}</p>
    <button @click="addCounter">Add</button>
  </div>
</template>

<script>
import { ref } from "vue";
export default {
  setup() {
    const num = 0;
    const counter = ref(num);
    console.log("---ref---");
    console.log(counter); // { value: 0 }
    console.log("---ref value---");
    console.log(counter.value); // 0

    const addCounter = () => {
      counter.value++;
      console.log("---after add---");
      console.log("---ref---");
      console.log(counter);
      console.log("---ref value---");
      console.log(counter.value);
      console.log("---original value---");
      console.log(num);
    };
    return { counter, addCounter };
  }
  methods: {
    resetCouter() {
        this.counter = 0;
    }
  }
};
</script>

🔨 以上示例点击 add 按钮后,控制台输出

ref
ref

提示

因为在 JS 中 NumberString基本类型是通过拷贝值(而非引用的方式)直接传递的,而对象和数组则是通过引用传递的,因此函数 ref() 将传入的值封装在一个对象中,这样就可以让不同数据类型的行为统一。

任何类型的数据都被「包裹」在 ref 对象中,再在整个应用中通过引用方式安全地传递,不必担心在某个地方失去它的响应性。

相应地,对于响应式变量需要谨慎地使用解构,因为拆分后的传递就可能不是引用式传递,可能会破坏数据的响应性。

ref
ref

ref 作用原理

当入参是基础数据类型,那么该原始值是被拷贝到 ref 对象中的,因此之后对响应式变量的值进行更改时,原始值并不受影响

如果入参是对象,则先通过 reactive 方法使该对象具有高度的响应式(深层嵌套的属性也具有响应性),返回一个 proxy,然后再将该 proxy「包裹」在 ref 对象中,作为对象唯一属性 value 的值,由于 reactive 方法是在原对象上加一层代理,内部依然指向原对象,所以之后对响应式变量的值进行更改时,原对象也会受影响

一个包含对象类型值的 ref 可以响应式地替换整个对象

js
const objectRef = ref({ count: 0 })

// 这是响应式的替换
objectRef.value = { count: 1 }
vue
<template>
  <div>
    <p>{{ counter }}</p>
    <button @click="addCounter">Add</button>
  </div>
</template>

<script>
import { ref } from "vue";
export default {
  setup() {
    const obj = {
      num: 0
    };
    const counter = ref(obj);
    console.log("---ref---");
    console.log(counter); // { value: proxy }
    console.log("---ref value---");
    console.log(counter.value.num); // 0

    const addCounter = () => {
      counter.value.num++;
      console.log("---after add---");
      console.log("---ref---");
      console.log(counter);
      console.log("---ref value---");
      console.log(counter.value.num);
      console.log("---original value---");
      console.log(obj);
    };
    return { counter, addCounter };
  }
};
</script>

🔨 以上示例点击按钮后,控制台输出

ref accept object
ref accept object

reactive

reactive 函数接收一个对象,返回一个 proxy,相当于在原对象上加一层代理返回对象的响应式副本,而且响应式转换是支持深层嵌套的属性

提示

reactive 返回对象的响应式副本,相当于在原对象上加一层代理实现响应性,因此操作该对象时和普通对象一样,访问和修改对象的属性时,无需像 ref 对象通过 value 属性「中转」。

提示

由于 reactive 方法是在原对象上加一层代理,内部依然指向原对象,所以之后对响应式变量的值进行更改时,原对象也会受影响

vue
<template>
  <div>
    <p>{{ counter }}</p>
    <button @click="addCounter">Add</button>
  </div>
</template>

<script>
import { reactive } from "vue";
export default {
  setup() {
    const obj = {
      num: 0,
      nestProp: {
        nestNum: 10
      }
    };
    const counter = reactive(obj);
    console.log("---proxy---");
    console.log(counter); // proxy

    const addCounter = () => {
      counter.num++;
      counter.nestProp.nestNum++;
      console.log("---after add---");
      console.log("---proxy---");
      console.log(counter);
      console.log("---original value---");
      console.log(obj);
    };
    return { counter, addCounter };
  }
};
</script>

🔨 以上示例点击按钮后控制台的输出

reactive
reactive

提示

如果接受一个 ref 对象,reactive 会自动解包内部值,使其行为类似于普通 property,这样访问 proxy 对象的属性时就无需通过 value 属性了。也可以通过赋值的方式传递 ref 对象。

Ref 解包仅发生在被深层响应式 Object(即该对象通过 reactive 进行包裹处理)嵌套的时候。当从 Array 或原生集合类型如 Map 访问 ref 对象时,不会进行解包

js
const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

// ref 会自动解包,所以不需要使用 state.count.value 的方式
state.count = 1
console.log(count.value) // 1

const otherCount = ref(2)

// 将一个新的 ref 赋值给一个关联了已有 ref 的属性
// 那么它会替换掉旧的 ref
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1

// ref 作为数组的元素
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

// ref 作为集合的元素
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
注意

不应该reactive 函数返回的 proxy 对象进行解构,因为这可能会导致变量的响应性丢失。

例如真的需要解构 props,可先使用函数 toRefs(props)props 对象的每个属性(即每个 prop)包裹为一个 ref 对象,这样 props 对象解构后分配给每个变量都指向源对象的属性,依赖保持了数据的响应性。

如果解构后,变量「接到」的值是基础数据类型,那么变量就会丢失响应性,由于代理实现的响应性针对的是原对象,这样改变对象的属性值后,页面模板相应的数据也无法更新。

而如果使用 toRefs 方法,将每个属性都包裹为一个 ref 对象,那么解构后各属性值依然可以保证是通过引用方式传递给各个变量的,这样就可以维持响应性,对象的属性值修改后,页面模板的相应数据也响应式地更新。

🔨 以下示例是点击按钮后控制台的输出,其中 num 是 proxy 直接解构得到的变量,numRef 是 proxy 经过 toRefs() 处理后解构得到的变量。

reactive toRefs
reactive toRefs

维持响应性

toRefs

函数 toRefs(obj) 将为传入的对象的每个属性值包裹一个 ref 对象,这样对象解构后属性值(通过引用方式)分配给的每个变量,这些变量依然都指向源对象的属性,依然保持了数据的响应性。

注意

一般传入 reactive 函数返回的 proxy 对象,以便在 setup 选项最后以解构的形式抛出对象的属性,方便在模板中调用。

如果传入 toRefs 函数的是普通的对象,即数据原来就不具有响应性,那么最后解构得到的变量也不会是响应式的(更改变量的值后,页面模板的数据不会相应地更新)。

toRef

函数 toRef(obj, propertyName) 将给定对象上指定的属性值包裹一个 ref 对象,然后单独「抽出」该属性值(赋值给变量,该变量指向源对象的属性),进行传递时仍可保持数据的响应性。

可以将它理解为 toRefs 函数的「单数」版本。

提示

toRefs 函数不同,toRef 即使源对象的指定的 property 不存在,也会返回一个可用的 ref。

这使得它在使用可选 prop 时特别有用,因为可选 prop 并不会被 toRefs 处理

计算属性和侦听器

computed

使用从 Vue 导入的 computed 函数,可以在 Vue 的 computed 组件外部创建计算属性。

它接受一个 getter 函数,并且 getter 返回的值是一个不可变的响应式 ref 对象,因此在访问计算属性的值,需要通过属性 value

如果在模板中需要使用这个计算属性,记得将它放在 setup 最后 return 的对象里。

提示

其中依赖的响应式变量可以通过 refreactive 创建。

js
import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
提示

虽然不常见,但可以为 computed 设置一个 setter 函数,这样就可以创建一个可写的 ref 对象,setter 函数的作用一般是用传递进来的值,反过来改变 computed 依赖的响应式数据。

js
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

watch

使用从 Vue 导入的 watch 函数,其功能就像选项 API watch 和组件实例的方法 vm.$watch 一样,监听响应式变量,在数据变动时执行相应的操作。

函数 watch 接受 3 个参数:

  • 第一个参数:需要侦听的响应式引用或 getter 函数(返回响应式数据)
    注意

    在组件中常见的错误是直接监听 props 对象的某个属性 props.propertyName,这可能会丢失响应性(直接监听 props 对象的属性相当于将其解构)

    正确方式应用是以函数的形式进行侦听该函数返回相应的 props 对象的某个属性

    js
    watch(
      // 以箭头函数的形式返回 props 的某个属性
      () => props.count,
      (newCount, prevCount) => {
        /* ... */
      }
    )
    
  • 第二个参数:一个回调函数(在其中执行副作用
  • 第三个(可选)参数:一个配置选项对象
    • deep 为了侦听对象或数组内部嵌套值的变化
    • immediate 立即触发回调函数
    • flush 可以更好地控制回调的时间。它的值可以设置为 'pre'(默认值,在更新渲染前被调用)、'post'(将回调推迟到更新渲染之后)或 'sync'(同步进行)
js
import { ref, reactivewatch } from 'vue'

const counter = ref(0)
// 侦听一个 ref
watch(counter, (newValue, oldValue) => {
  console.log('The new counter value is: ' + counter.value)
})

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 侦听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

// 可以侦听数组部分的操作
const numbers = reactive([1, 2, 3, 4])

watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers)
  }
)

numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

// 深度监听
const state = reactive({
  id: 1,
  attributes: {
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
    console.log(
      'deep',
      state.attributes.name,
      prevState.attributes.name
    )
  },
  { deep: true } // 配置选项 deep 进行深度监听
)

// 打印出来的值始终是当前值
state.attributes.name = 'Alex' // "deep" "Alex" "Alex"
提示

由前面示例的结果可知,侦听一个响应式对象或数组时,将始终返回该对象的当前值的引用。为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝,这样就可以获取当前状态和上一个状态的值

可以通过诸如 lodash.cloneDeep 这样的实用工具来实现,也可以使用 JS 提供的全局方法 structuredClone()

js
import _ from 'lodash'

const state = reactive({
  id: 1,
  attributes: {
    name: '',
  }
})

watch(
  () => _.cloneDeep(state),
  (state, prevState) => {
    console.log(
      state.attributes.name,
      prevState.attributes.name
    )
  }
)

// 通过深拷贝监听的对象,可以获取当前状态和上一个状态的值
state.attributes.name = 'Alex' // "Alex" ""

可以通过显式调用 watch() 函数的返回值,以手动停止侦听(默认是在组件卸载时自动停止侦听)

js
const counter = ref(0)

// watch a ref
const unWatch = watch(counter, (newValue, oldValue) => {
  console.log('The new counter value is: ' + counter.value)
})

// later, teardown the watcher
unWatch()
提示

watchEffect 比较,watch 允许我们

  • 惰性地执行副作用,即默认情况下只有在侦听源发生更改时才执行回调,而 watchEffect 必须先执行一次回调以「搜集」依赖。
  • watch 需要更具体地说明应触发侦听器重新运行的状态,显式地声明需要侦听的响应式引用
  • 访问被侦听状态的先前值和当前值

watchEffect

为了根据响应式状态变更时执行相应的操作,可以使用 watchEffect 方法。它接受传入一个称为副作用的函数,然后就可以在其依赖变更时,自动重新运行该函数

与侦听器 watch 不同,没有显式地指定要侦听的响应式变量,而是直接传入的是一个函数(执行副作用操作,用一个函数包裹),因此它会先立即执行一次传入的函数,自动收集和跟踪响应式依赖

watchEffect 的优势

watch 需要显式地声明追踪的响应式变量,更加精确地控制回调函数的触发时机

相对而言用 watchEffect 创建侦听器更简便,可以消除手动维护依赖列表的负担

特别是需要侦听多个依赖项时(例如需要侦听一个嵌套数据结构中的几个属性),使用 watchEffect() 会比深度侦听器更高效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性

js
const count = ref(0)

watchEffect(() => console.log(count.value))
// 0

setTimeout(() => {
  count.value++
  // 1
}, 100)
注意

watchEffect 仅会对回调函数里同步执行的响应性依赖进行追踪,即对于异步回调,只有在第一个 await 前访问到的响应性属性才会被追踪。

类似地,也是可以通过显式调用 watchEffect() 函数的返回值,以手动停止侦听(默认是在组件卸载时自动停止侦听)

js
const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

当一个副作用函数进入队列时,默认情况下会在所有的组件 update 前执行,如果需要在组件更新后再运行侦听器副作用,可以在创建侦听器时,传递带有 flush 选项并设置为 post(默认为 'pre'

js
// 在组件更新后触发,这样就可以访问更新后的 DOM
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)
提示

更多用法可以参考官方文档这篇文章

provide 和 inject

使用从 vue 导入的 provideinject 方法,在 setup 选项中实现从祖先组件向其子孙后代组件传值,这种方法不论组件层次嵌套有多深都可以进行数据传递

provide 函数接收两个参数,第一个参数称为注入名(定义要 provide 的 property 的名称),可以是一个字符串或是一个 Symbol;第二个参数是相应的值。

js
import { provide } from 'vue'

export default {
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
以 Symbol 作为注入名

对于大型的应用,可能包含非常多的依赖提供,或者编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突

推荐在一个单独的文件中导出这些注入名 Symbol

keys.js
js
// 该文件专门创建并导出一系列 Symbol 作为注入名
export const myInjectionKey = Symbol()
parent.vue
vue
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要提供的数据
*/ });
child.vue
vue
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

inject 函数接收两个参数,第一个参数是字符串,定义要 inject 的 property 的名称;第二个(可选)参数是设定默认值

js
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeoLocation = inject('geolocation')
  }
}
提示

为了增加 provide 值和 inject 值之间的响应性,可以在 provide 值时先使用 refreactive 函数进行处理。这样当祖父级数据改变时,接收数据的后代组件也会相应更新。

生命周期钩子

setup 选项中注册生命周期钩子的方法,与选项式 API 的名称相同,一般是添加前缀 on,以下是生命周期的选项式 API 和组合式 API 之间的映射关系

选项式 API 所提供的钩子函数setup() 函数里可以使用的钩子函数
beforeCreate(不需要相应的生命周期钩子)setup() 运行时间就是在 beforeCreate 更前
created(不需要相应的生命周期钩子)使用 setup() 代替
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

这些函数接受一个回调,在特定的时间点,当钩子被组件调用时,该回调将被执行

js
import { onMounted } from 'vue'

// ...
// 在我们的组件中
setup() {
  onMounted(() => {
      console.log('mounted') // 在 `mounted` 时执行
  })
}

模板引用

函数 ref 除了可以为数据建立响应式引用,还可以为模板建立引用,两者概念其实是统一的。

为了获得对模板内元素或组件实例的引用,需要进行以下步骤:

  • 先在 setup() 声明一个初始值为 null 的 ref 对象(记得在 setup() 最后返回这个响应式变量)
  • 然后在模板所需引用的节点上设置 ref 属性,其值与以上「抛出」的响应式变量名一致
  • 然后就可以onMounted 钩子的回调函数中(即组件挂载后,通过这个响应式变量的 value 属性访问到相应的 DOM 节点
html
<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM 元素将在初始渲染后分配给 ref
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

可以使用 watchwatchEffect 方法来侦听模板引用,但是为了与 DOM 保持同步并引用正确的元素,侦听器应该用 flush: 'post' 配置

vue
<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, watchEffect } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      watchEffect(() => {
        console.log(root.value) // => <div>This is a root element</div>
      },
      {
        flush: 'post'
      })

      return {
        root
      }
    }
  }
</script>

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素,但是 ref 数组并不保证与源数组相同的顺序

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

const list = ref([
  /* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>
提示

如果在使用组件时,为组件添加 ref 属性,则最后得到的响应式引用是组件的实例(而不是 DOM 节点),具体可以查看另一篇笔记的相关内容

模块化

如果将组件的所有功能都写在 setup 选项中,代码量会很大而变得难以维护,可以基于功能将逻辑代码抽离到各个组合式函数中,这些函数名一般以 use 作为前缀。

说明

组合式函数 Composables 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数

一般而言,函数封装的是无状态的逻辑以便复用,它在接收一些输入后立刻返回所期望的输出。而封装了有状态逻辑的函数,会负责管理会随时间而变化的状态(即这一类函数的返回值中一般会包含关于状态的变量)

将代码划分为独立的各个功能模块,一般是以 use 为前缀(与其中的函数同名)的 JavaScript 文件,再在组件中引入使用。

src/composable/useCounter.js
js
import { ref, onMounted } from 'vue'

// 导出组合式函数
export default function useCounter(user) {
  onMounted(() => {
      console.log('mounted')
  })
  const num = 0;
  const counter = ref(num);

  const addCounter = () => {
    counter.value++;
  };

  // 通过返回值暴露所管理的状态 counter
  return { counter, addCounter };
}
vue
<template>
  <div>
    <p>{{ counter }}</p>
    <button @click="addCounter">Add</button>
  </div>
</template>

<script>
// 引入自定义的组合式函数
import useCounter from '@/composable/useCounter'
import { ref } from "vue";

export default {
  setup(props) {
    const user = 'Ben';
    const num = 0;
    const { counter, addCounter } = useCounter(user);

    return { counter, addCounter };
  }
};
</script>
组合式函数的限制

组合式函数在 <script setup>setup() 钩子中,应始终被同步地调用(这里的同步调用是指组合式函数不能包裹在 Promise 或 setTimeout 等函数中 ❓ 但是组合式函数本身可以是异步函数,即组合式函数内可以进行一些异步操作的)

这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,这样才能够将组合式函数中的相关任务完成

  • 将组合式函数中的生命周期钩子注册到该组件实例上
  • 将组合式函数中的计算属性和监听器注册到该组件实例上(以便在该组件被卸载时停止监听,避免内存泄漏)

但是 <script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。

最佳实践

在组合式函数所封装的逻辑中会有随时间而变化的状态,所以需要采用响应式变量,推荐使用 ref()不是 reactive() 创建响应式变量

如果组合式函数最后返回的值(一个对象)是使用 reactive() 所创建的响应式变量,在组件中使用时如果解构该对象,就会造成响应性丢失,与之相反 ref 则可以维持这一响应性连接。

js
// x 和 y 是两个 ref
const { x, y } = useMouse()

所以推荐对每个响应式变量都采用 ref() 生成,最终再分别写到返回的对象中(作为对象的各个属性)

如果希望以对象属性的形式来使用组合式函数中返回的状态(不带有 .value 这个繁琐的后缀 ❓),可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包

my-component.vue
vue
<script setup>
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
</script>
<template>
  <p>
    Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
  </p>
</template>

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes