一篇带你了解Vue3组合式API

697 阅读13分钟

三、vue3组合式API

1、组合式API

1.1 什么是组合式API

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api。在 Vue 3 中,组合式 API 基本上都会配合 `` 语法在单文件组件中使用。

1.2 为什么使用它

1.2.1 更好的逻辑复用#

组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷

组合式 API 提供的逻辑复用能力孵化了一些非常棒的社区项目,比如 VueUse,一个不断成长的工具型组合式函数集合。组合式 API 还为其他第三方状态管理库与 Vue 的响应式系统之间的集成提供了一套简洁清晰的机制,例如 RxJS

1.2.2更灵活的代码组织#

许多用户喜欢选项式 API 的原因是因为它在默认情况下就能够让人写出有组织的代码:大部分代码都自然地被放进了对应的选项里。然而,选项式 API 在单个组件的逻辑复杂到一定程度时,会面临一些无法忽视的限制。这些限制主要体现在需要处理多个逻辑关注点的组件中,这是我们在许多 Vue 2 的实际案例中所观察到的。

我们以 Vue CLI GUI 中的文件浏览器组件为例:这个组件承担了以下几个逻辑关注点:

1.2.3 更好的类型推导#

近几年来,越来越多的开发者开始使用 TypeScript 书写更健壮可靠的代码,TypeScript 还提供了非常好的 IDE 开发支持。然而选项式 API 是在 2013 年被设计出来的,那时并没有把类型推导考虑进去,因此我们不得不做了一些复杂到夸张的类型体操才实现了对选项式 API 的类型推导。但尽管做了这么多的努力,选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。

因此,很多想要搭配 TS 使用 Vue 的开发者采用了由 vue-class-component 提供的 Class API。然而,基于 Class 的 API 非常依赖 ES 装饰器,在 2019 年我们开始开发 Vue 3 时,它仍是一个仅处于 stage 2 的语言功能。我们认为基于一个不稳定的语言提案去设计框架的核心 API 风险实在太大了,因此没有继续向 Class API 的方向发展。在那之后装饰器提案果然又发生了很大的变动,在 2022 年才终于到达 stage 3。另一个问题是,基于 Class 的 API 和选项式 API 在逻辑复用和代码组织方面存在相同的限制。

相比之下,组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!这也让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。

1.2.4 更小的生产包体积#

搭配 <script setup> 使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需一个代码实例从中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。

2、setup()函数

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

其他情况下,都应优先使用 <script setup> 语法。

2.1 基本使用

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性

请注意在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

2.2 访问 Prop

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

{
  props: {
    title: String,
    count: Number
  },
  setup(props) {
    console.log(props.title)
    console.log(props.count)
  }
}

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。

如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()toRef() 这两个工具函数

2.3 Setup的上下文

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

{
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }
}

该上下文对象是非响应式的,可以安全地解构:

{
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

attrsslots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.xslots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrsslots 的属性都不是响应式的。如果你想要基于 attrsslots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

在父组件通过ref获取子组件的实例的属性和方法的需求中,需要注意:

1.如果子组件是 选项式API组件,基本不需要做任何操作

2.如果子组件是 组合式API组件,需要通过 context.expose 暴露给父组件需要使用的属性和方法

3.如果父组件使用 选项式API, 可以通过 this.$refs.refName 访问到子组件想要你看到的属性和方法

4.如果父组件使用 组合式API,需要在setup中先创建 refName,然后再访问子组件想要你看到的属性和方法(const refName = ref() refName.value.X)

3、响应式核心

3.1ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。

将一个 ref 赋值给为一个 reactive 属性时,该 ref 会被自动解包

3.2computed ()

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

3.3 reactive()

返回一个对象的响应式代理。

响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

3.4 readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 用来做响应性追踪
  console.log(copy.count)
})

// 更改源属性会触发其依赖的侦听器
original.count++

// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning

3.5 watchEffect()

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求。

返回值是一个用来停止该副作用的函数。

3.6 watch()

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

  • watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    第一个参数是侦听器的。这个来源可以是以下几种:

    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组

    第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

    当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

    第三个可选的参数是一个对象,支持以下这些选项:

    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器

    watchEffect() 相比,watch() 使我们可以:

    • 懒执行副作用;
    • 更加明确是应该由哪个状态触发侦听器重新执行;
    • 可以访问所侦听状态的前一个值和当前值。

4、工具函数

4.1 toRef()

基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

4.2 toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

5、生命周期钩子

5.1 onMounted()

注册一个回调函数,在组件挂载完成后执行。

组件在以下情况下被视为已挂载:

  • 其所有同步子组件都已经被挂载 (不包含异步组件或 <Suspense> 树内的组件)。
  • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端执行。

5.2 onUpdated()

注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。

父组件的更新钩子将在其子组件的更新钩子之后调用。

这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

5.3 onUnmounted()

注册一个回调函数,在组件实例被卸载之后调用。

一个组件在以下情况下被视为已卸载:

  • 其所有子组件都已经被卸载。
  • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。

可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

5.4 onBeforeMount()

注册一个钩子,在组件被挂载之前被调用。

当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。

5.5 onBeforeUpdate()

注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。

这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

5.6 onBeforeUnmount()

注册一个钩子,在组件实例被卸载之前调用。

当这个钩子被调用时,组件实例依然还保有全部的功能。

6、依赖注入

6.1 provide()

提供一个值,可以被后代组件注入。

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

6.2 inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用

7、组合式函数

7.1 什么是“组合式函数”?

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

也可以叫做自定义hooks

当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。