通过一个demo学习Vue 3 Composition API

2,496 阅读5分钟

前言

9月18日,Vue 3.0正式发布。其中,Composition API可谓是新版本的一大亮点。本文将通过一个小demo简单介绍Composition API的主要功能和使用方式。同时,我们将使用Vue团队发布的一个新的构建工具Vite作为这个小demo的构建工具。

Composition API是什么?

Vue 3的官方文档现在可以在v3.vuejs.org中查看(目前还未上线中文文档)。在文档中找到Composition API,可以查看该API的详细介绍。通过官方文档我们知道,Composition API主要是为了解决规模较大的Vue组件的可维护性、灵活性和可复用性的问题。传统的Vue组件将组件分成了propscomponentsdatamethodscomputedwatch及多个声明周期钩子等多个部分。组件的各个逻辑共能也分散在这多个部分中,当组件具有一定的规模后,代码逻辑就会出现支离破碎的情况,极大地影响了代码的可维护性等。

为了解决这个问题,Vue团队提出了将大组件中的代码按照逻辑关系组织在一起,这就促成了Composition API的诞生。当组件出现下述情况,使用Composition API可能会更好地避免问题的出现:

  • 某部分代码可以在多个组件中重复利用,且想使用其他方法代替mixins或slot
  • 确保类型安全(尤其是在使用TypeScript时)

搭建demo

首先我们使用Vite构建命令初始化这个项目vue3-composition-api-demo(顺便提一嘴,Vite也可以用来构建react项目)

# 初始化项目
npm init vite-app vue3-composition-api-demo
# 进入目录
cd vue3-composition-api-demo
# 安装依赖
npm install
# 运行项目
npm run dev

开发环境的项目构建速度比起webpack是真的快了很多,得益于原生ES Module的支持。类似的构建工具还有Snowpack,感兴趣的小伙伴可以去了解了解。

Composition API的基本使用

Composition API在Vue 3中开箱即用,如果想在Vue 2中使用Composition API,可以在项目中添加@vue/composition-api插件。

首先我们写一些模板代码:

<template>
  <div>
    <p>Spaces left: {{ spacesLeft }} out of {{ capacity }}</p>
    <h2>Attending</h2>
    <div>
      <button @click="increaseCapacity">Increase Capacity</button>
    </div>
    <div>
      <button @click="addAttending">addAttending</button>
    </div>
    <div>
      <button @click="sort">sort</button>
    </div>
    <!-- 使用vue的过渡动画组件演示,嫌麻烦的同学也可以不用(语气逐渐王刚化) -->
    <transition-group name="flip-list" tag="ul" :style="{width: '200px', margin: '20px auto'}">
      <li v-for="item in attending" :key="`Attending${item}`">
        {{ item }}
      </li>
    </transition-group>
  </div>
</template>

Vue模板中使用了两个data变量capacityattending,一个computed属性spacesLeft和几个相关的函数。

接着我们在script标签中定义它们。我们将借助setup方法,通过两种写法实现这些功能。

写法一

// 引入ref和computed
import { ref, computed } from "vue"
export default {
  setup() {
    const capacity = ref(4)
    const attending = ref(["Tim", "Bob", "Joe"])
    // computed计算属性的声明方式
    const spacesLeft = computed(() => {
      // 注意,通过ref声明的响应式数据需要通过.value拿到该值
      return capacity.value - attending.value.length
    })
    // 代替methods声明方法
    function increaseCapacity () {
      capacity.value++
    }
    function sort () {
      attending.value = lodash.shuffle(attending.value)
    }
    function addAttending () {
      attending.value.push(randomName(lodash.random(3, 7)))
    }
    // 通过一个对象暴露以上变量和方法
    return {
      capacity,
      attending,
      spacesLeft,
      increaseCapacity
    }
  }
}

其中,ref()接收一个value值作为入参,基本类型的入参将被包裹在一个object中(如capacity),通过proxy实现响应式(这很Vue 3)。computed计算属性,是通过computed()方法声明的(全部替换成了函数式的声明),注意computed中,我们需要调用capacity.valueattending.value来获取数据的值。最后,所有在template中使用的变量和方法通过return暴露出去。我们再来看看第二种写法。

写法二

import { reactive, computed, toRefs } from "vue"
export default {
// 将需要使用的变量添加到event的属性中
  setup(props, context) {
    const event = reactive({
      capacity: 4,
      attending: ['Tim', 'Bob', 'Joe'],
      spacesLeft: computed(() => { return event.capacity - event.attending.length })
    })
    // 方法的声明方法不变
    function increaseCapacity () {
      event.capacity++
    }
    function sort () {
      event.attending = lodash.shuffle(event.attending)
    }
    function addAttending () {
      event.attending.push(randomName(lodash.random(3, 7)))
    }
    // toRefs()
    return {...toRefs(event), increaseCapacity, addAttending, sort}
  }
}

改变的地方是toRefs()toRefs()方法接受一个Reactive 实例作为入参,返回一个响应式的object。在setup()函数体内直接通过event属性拿到相关变量值,不需要额外加上.value。两种写法在使用场景上略有区别。我个人更喜欢第二种写法,return更加简洁优雅👍。

最后,这个页面看起来应该是这样的:

最后,这个页面看起来应该是这样的

setup方法

我们来看看setup()方法。

setup(props, context) {
  // Attributes (Non-reactive object)
  console.log(context.attrs)

  // Slots (Non-reactive object)
  console.log(context.slots)

  // Emit Events (Method)
  console.log(context.emit)
}

setup()方法接受2个参数,第一个参数props,顾名思义就是Vue的propssetup()执行时还没有vue实例,所以在setup()函数体内拿不到datacomputedmethods,所以需要借助第二个参数context拿到vue实例相关的数据,其中包含attrsslotsemit

setup生命周期hooks

setup()内部有相应的vue生命周期hooks供开发者使用。官方文档有具体的罗列。

我们来对比打印一下新旧两版生命周期hooks:

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onUnmounted
} from "vue"

function printLifeCycle () {
  onBeforeMount(() => {console.log('onBeforeMount')})
  onBeforeUpdate(() => {console.log('onBeforeUpdate')})
  onMounted(() => {console.log('onMounted')})
  onUpdated(() => {console.log('onUpdated')})
  onUnmounted(() => {console.log('onUnmounted')})
}

export default {
  // setup内部生命周期hooks
  setup(props, context) {
    printLifeCycle()
  },
  // 老生命周期hooks
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  }
}

setup()内部的hooks执行早于vue实例上的老hooks。打印顺序如下:

彩蛋 - Vite引入第三方库

细心的同学应该发现了我在demo中使用了lodashshufflerandom。在Vite中引入部分未使用ES Module的依赖会报错找不到该依赖。需要注意,在vite.config.js中的optimize配置下加入该依赖:

module.exports = {
  optimizeDeps: {
    include: ["lodash"]
  }
}

在引入lodash时,需要注意,这样会报错:

import { random, shuffle } from 'lodash'

这样也会报错:

import random from 'lodash/random'
import shuffle from 'lodash/shuffle'

所以目前只能够乖乖全部引入lodash:

import lodash from 'lodash'

算是ES Module的一个小坑吧,相关问题可以参考这篇博客vite 尝试和入门