Vue3.x 从零开始(三)—— 使用 Composition API 优化组件

时间:2020-10-12 19:56:00 来源:互联网 作者: 神秘的大神 字体:

在《Vue3.x 从零开始(二)》中已经介绍了 Mixin 这种抽取公共逻辑的方式 

但 Mixin 提供的数据或函数,无法在组件中直观的体现出来

这导致组件的维护人员需要非常熟悉被引入的 Mixin 对象,不然出现重名的情况就容易出现意料之外的 bug

而 Composition API 是以函数的形式封装公共逻辑,它通过显式的返回一个对象,让开发人员能在组件中直接了解到被引入的字段

 

 

一、简单示例

假如某个组件需要在加载完成后,根据父组件传入的参数来发送请求,并渲染请求结果:

这些逻辑可以直接提取为 mixin 对象,以达到复用的目的

也可以通过 Composition API 将这部分逻辑封装成一个函数

在这个例子中,我们将相应的逻辑代码都放到了 setup 函数中,它是 Composition API 的入口

setup 函数在组件创建之前就会触发,最终返回一个我们定义的对象,这个对象的字段可以在组件的其他地方直接使用

比如上例中返回了变量 list 和函数 fetchList,我们可以直接在组件的 methods 或者其他地方,通过 this.list 、this.fetchList 使用他们

在实际项目中,可以将上面的逻辑单独封装成一个函数并在 setup 中调用,最终返回函数的返回值

 

对比一下上面的两段代码,你应该会对 setup 有一个简单的认识,接下来我会详细介绍 setup 和 Composition API

 

 

二、生命周期和 setup

每个 Vue 组件都有一个属于自己的生命历程:

created(创建)-> mounted(加载)-> updated(更新)-> unmounted(卸载)

这些过程会对应一个钩子函数,这些钩子函数会在相应的阶段触发

上面只提到了四个生命周期,完整的生命周期可以参考《生命周期图示》

// vue 2 中的 destroyed 和 beforeDestroy 钩子在 vue 3 中被重命名为 unmountedbeforeUnmount

setup 函数是在解析其它组件选项之前,也就是 beforeCreate 之前执行

 

所以在 setup 内部,this 不是当前组件实例的引用,也就是说 setup 中无法直接调用组件的其他数据

如何在不能直接使用组件数据的前提下,完成组件的业务逻辑呢?

于是 Composition API 登场了,它让我们能够在 setup 中使用生命周期钩子函数、响应式数据等组件特性

别急,这些内容待会儿就会介绍,目前还是先把 setup 弄清楚

 

1. setup 不是生命周期钩子函数!

它只是基于 beforeCreate 运行,但函数内部无法通过 this 获取组件实例

而且 setup 有着生命周期钩子不具备的参数:props 和 context

setup(props, context) { // 组件 props
 console.log(props); const { attrs, slots, emit } = context; // Attribute (非响应式对象)
 console.log(attrs); // 组件插槽 (非响应式对象)
 console.log(slots); // 触发事件 (方法)
 console.log(emit); }

这里的 props 就是组件内部定义的 props,是由父组件传入的参数

假如父组件传入了一个属性名为 title 的 props,在 setup 中可以通过 props.title 直接取到

而且这个 props 是响应式的,当传入新的 prop 时,props 会被更新

但正是因为 props 是响应式的,所以不能直接对 props 使用 ES6 解构,因为它会消除 prop 的响应性

Vue 3 提供了一个 toRefs 全局方法来解决这个问题:

import { toRefs } from 'vue'; 
setup (props) { // 通过 toRefs 包装后的 props 可以在 ES6 解构之后依然具有响应性
    const { title } = toRefs(props); console.log(title); }

 

2. 在 setup 中注册生命周期钩子

如果是在 setup 内部注册生命周期钩子,则需要从 Vue 引入带有 on 前缀的生命周期工具函数

import { toRefs, onMounted } from 'vue'; 
setup (props) { // 使用 `toRefs` 创建对prop的 `title` property 的响应式引用
    const { title } = toRefs(props); // 注册生命周期钩子
    onMounted(() => { console.log('onMounted', title); }); }

上面的 onMounted 会接收一个函数作为参数,在组件中对应的生命周期钩子 (mounted) 触发时,会执行这个被传入的函数

除了 onMounted 以外,其他 created 之后的生命周期也有对应的注册函数,如 onUpdated、onBeforeUnmount 等

setup 中并没有 beforeCreatecreated 这两个生命周期的注册函数

因为 setup 本身是基于 beforeCreate 运行,会用到 beforeCreate 和 created 的场景都可以通过 setup 替代

也就是说,在 beforeCreate 和 created 中编写的任何代码都应该直接在 setup 函数中编写

 

3. setup 的返回值

如果 setup 显式的返回了一个对象,这个对象的所有内容都会暴露给组件的其余部分

setup() { return { name: 'wise wrong', foo: (text: string) => { console.log(`Hello ${text}`); }, }; }, mounted() { // 直接使用 setup 的返回值
  this.foo(this.name); },

上面的 setup 返回了一个 foo 函数,还有一个 name 字段,目前这个 name 还不是一个响应式变量

为此我们需要使用 ref 全局方法

import { ref } from 'vue'; 
setup (props) { return { name: ref('wise wrong'); } }

通过 ref 包装的变量会具有响应性,在组件中可以像正常的组件属性一样使用

但在 setup 内部,ref 包装后的变量需要通过 value 来修改变量的值

import { ref } from 'vue'; 
setup() { const name = ref('wise wrong'); onMounted(() => { // 在 setup 内部通过 value 修改变量值
    name.value = "Let's study Vue 3"; }); return { name, }; }, methods: { test() { // 在组件中可以直接修改 ref 变量
    this.name = 'good job'; }, },

除了返回一个对象以外,setup 还可以返回一个渲染函数

import { h, ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 返回一个渲染函数以覆盖 template
    return () => h('div', [readersNumber.value, book.title]) } }

如果直接返回了渲染函数,组件中定义的 template 将会失效

 

 

三、在 Setup 中使用 Computed、Watch

setup 的本意是用来替代 mixin,因此除了 data、methods、生命周期钩子之外,还有许多组件选项需要解决

这里先介绍 computed 和 watch

 

1. Computed

在组件中 computed 用来管理包含响应式数据的复杂逻辑

computed: { booksMessage() { return this.books.length > 0 ? 'Yes' : 'No' } }

计算属性可以在组件中直接使用,并会随着响应式数据的更新而更新

<template>
  <p>{{ booksMessage }}</p>
</template>

而在 setup 中,需要通过全局函数 computed 来包装

import { ref, computed } from 'vue' setup() { const books = ref([]); const booksMessage = computed(() => books.value.length > 0 ? 'Yes' : 'No'); return { books, booksMessage, } }

 

 2. Watch

 watch 用来监听数据的变化

可以通过 watch 定义对应的处理函数,当数据发生变化时候会调用该函数

data: () => ({ question: '', answer: 'good luck :)' }), watch: { question(newQuestion, oldQuestion) { // 当 question 变化的时候会执行该函数
    if (newQuestion.indexOf('?') > -1) { console.log('answer: ', this.answer); } } },

在 setup 中,也需要使用全局函数 watch 来包装

import { ref, watch } from 'vue' setup() { const question = ref(''); const answer = ref('good luck :)'); watch(question, (newValue, oldValue) => { if (newValue.indexOf('?') > -1) { console.log('answer: ', this.answer.value); } }); return { question, answer, } }

 

 

四、抽取公共逻辑

在了解了基本的 Composition API 之后,可以尝试将公共逻辑抽取出来单独维护

假设有以下两个逻辑点需要被抽取:

1. 在组件加载完成后,通过父组件传入的参数 query 请求数据列表 list,当 query 变化时需要重新请求并更新 list;

2. 根据关键字 keyword 从数据列表 list 中筛选数据,并使用 computed 记录结果。

 

这两段逻辑可以直接在 setup 中实现,但这样会使得 setup 看起来非常累赘

我们可以把这段逻辑拆成两个函数并单独维护

首先在项目目录 src 下创建一个新的文件夹 composables

然后创建 useList.js 文件,完成第一个逻辑:

// src/composables/useList.js
 import { ref, onMounted, watch } from 'vue'; // 发起接口请求
function fetchList() { // ...
} export default function useList(query) { // 创建数据列表 list (data)
  const list = ref([]); // 创建查询并更新 list 的方法 (methods)
  const getList = async () => { list.value = await fetchList(query); }; // 生命周期 mounted
 onMounted(getList); // 监听 query 的变化并更新 list
 watch(query, getList); return { list, getList, }; }

然后创建 useFilter.js 文件,完成第二个逻辑:

// src/composables/useFilter.js
 import { ref, computed } from 'vue'; export default function useList(list) { // 创建搜索关键字 keyword (data)
  const keyword = ref(''); // 创建筛选结果 filterRes (computed)
  const filterRes = computed(() => { return list.filter((value) => { return value.includes(keyword.value); }); }); return { keyword, filterRes, }; }

然后在组件中引入,并在 setup 返回组件需要的字段

import { defineComponent, toRefs } from 'vue'; import useList from '@/composables/useList'; import useFiter from '@/composables/useFilter'; export default defineComponent({ props: ['query'], setup(props) { const { query } = toRefs(props); const { list, getList } = useList(query); const { keyword, filterRes } = useFiter(list); return { keyword, // 筛选关键字
      getList, // 查询并更新数据列表
      list: filterRes, // 不需要返回未经筛选的列表
 }; }, });

 这样的代码是不是更加清爽了呢?