[TOC]

本文代码都是在 单文件组件 的基础上使用

前言

Vue3TypeScript 提供了更好的支持,大部分内容可以不用显示定义类型,就像是在写js一样轻松。
本篇文档着重介绍 Vue3 新推出的 Composition API 的使用。

本文不会赘述过多的概念,API也不会作详尽的解释,本文重点不在此,这些已经有官方文档来做了。
因此本文在介绍知识点的同时,会给出相关概念的文档链接。这样做的好处是:

  • 减少非必要的篇幅,由更权威、详细的官方文档来解释这些概念
  • 想深入了解的人不用去官方文档一个个查找对应的概念放在何处,更加方便迅速。

想要快速的了解 Vue3 的新内容,可以参考 v3 迁移指南 进行了解。


起步 - 应用实例

我们先从创建一个 Vue应用实例 开始。

Vue2 中的实例是通过 new Vue() 创建的,而 Vue3 提供了一个新的全局API:createApp 函数。
Vue3提供了许多新的 全局API,想要了解所有的全局API,可参考:全局API - API参考

使用 createApp 函数创建一个应用实例:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

我们如何注入全局组件、插件、指令等?使用 应用API

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

import router from '@/routers'
import HelloWord from '@/components/HelloWord.vue'
import FocusDirective from '@/directive/focus'

const app = createApp(App)
// 使用插件
app.use(router)
// 注册全局组件
app.component('HelloWord', HelloWord)
// 注册全局组件
app.directive('focus', FocusDirective)
// 全局 provide
app.provide('guide', 'vue3 guide')

// 挂载应用实例
app.mount('#app')

还可以直接使用链式调用,非常酷炫:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

import router from '@/routers'
import HelloWord from '@/components/HelloWord.vue'
import FocusDirective from '@/directive/focus'

createApp(App)
  .use(router)
  .component('HelloWord', HelloWord)
  .directive('focus', FocusDirective)
  .provide('guide', 'vue3 guide')
  .mount('#app')

基础 - 组件实例

创建完应用实例的下一步,我们该创建一个 组件 了。
在 Vue3 中,若使用ts,创建组件则必须要引入一个全局API defineComponent 函数以获取类型提示:

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  components: {},
  data() {
      return {}
  },
  setup() {
    console.log('setup!')
  },
})
</script>

如上所示:

  • 要使用TypeScript,首先得在 <script> 标签内加上 lang="ts"
  • 其次要引入全局API defineComponent,传入一个组件选项对象。
  • Vue3 新增了 setup 组件选项,该选项就是 Composition API 的核心。

组件选项

在使用 setup 选项进行 Composition API 的开发之前,我们先来了解一下跟 Composition API 关系不大的一些选项。
若要了解所有的选项,请参考:选项式API - API参考

已废弃的选项

filters(过滤器)在 Vue3 中已被废弃,详情可查看 过滤器 - v3 迁移指南,官方推荐使用计算属性或方法来替代过滤器。

与Composition无关的常用选项

还有些常用的选项无需使用Composition API或与之无关,使用时还是直接声明在选项当中:


---

<a name="aSlxs"></a>
# Setup - Composition API 核心
Composition API(组合式API)是 Vue3 新推出的高可重用的语法,写在 `setup` 选项函数中。对于它的概念本处不再赘述,直接看官方文档即可。

- 想了解什么是组合式API,以及为什么使用它,请参考 [组合式API - 教程](https://v3.cn.vuejs.org/guide/composition-api-introduction.html#%E4%BB%8B%E7%BB%8D) 。

组件选项 `setup` 是 Composition API 的核心。Composition API 的代码就写在 `setup` 当中。<br />想更详细的了解 `setup` ,请参考:

- [setup - 教程](https://v3.cn.vuejs.org/guide/composition-api-setup.html)
- [setup - API参考](https://v3.cn.vuejs.org/api/options-composition.html#setup)
<a name="Y0YJk"></a>
## 接收参数
`setup` 接收两个参数:

- `props` 第一个参数,响应式对象。
- `context` 第二个参数,普通对象。包含三个组件 property。

若想解构使用请参考上述文档。
```typescript
export default defineComponent({
  name: 'App',
  setup(props, context) {
    // 访问props
    console.log(props.title)

    // Attribute (非响应式对象)
    console.log(context.attrs)

    // 插槽 (非响应式对象)
    console.log(context.slots)

    // 触发事件 (方法)
    console.log(context.emit)
  },
})

结合模板使用

setup 函数中返回的对象可以在模板中直接使用(没返回就无法使用):

<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>

<script lang="ts">
import { defineComponent, ref, reactive } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })

    // expose to template
    return {
      readersNumber,
      book,
    }
  },
})
</script>

注意:setup 函数中返回的 ref 在模板中是被 自动展开 的。因此在模板中不应使用 .value


Data - 数据

在 Composition API 中,要创建一个响应式的值,需要用到 refreactive 两个方法。

若想详细的了解这两个方法,可以参考:

本篇不说明用哪种会更好,不同应用场景下有不同的使用方法,最好是ref和reactive都一起使用,这样才能了解你的需求和哪个更为符合。

reactive

reactive可以生成一个对象的响应式副本

示例:

<template>
  <!-- 20 -->
  <p>{{ state.age }}</p>
  <!-- 更新后的信息 -->
  <p>{{ state.info.msg }}</p>
</template>

<script lang="ts">
import { defineComponent, onMounted, reactive } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      name: '李雷',
      age: 18,
      info: {
        msg: '信息',
      },
      tags: ['勤奋'],
    })

    onMounted(() => {
      state.age = 20
      state.info.msg = '更新后的信息'
    })

    return {
      state,
    }
  },
})
</script>

修改reactive包裹的对象的属性,模板中会同步更改。
注意:直接替换整个 state 对象,会导致响应性丢失!如:state = {age: 10},页面将不会进行更新。

TypeScript支持:

reactive默认会将传入的值作为返回值的类型,可参考 类型声明 reactive - 教程

reactive包裹的对象即使增加了新的属性,模板也能够得到响应式的变更。这在vue2中是行不通的(除非使用$set)。我们假设出现如下情况:

<template>
  <!-- 20 -->
  <p>{{ state.age }}</p>
  <!-- 韩梅梅 -->
  <p>{{ state.name }}</p>
</template>

<script lang="ts">
import { defineComponent, onMounted, reactive } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      age: 18,
    })

    onMounted(() => {
      state.age = 20
      // Property 'name' does not exist on type '{ age: number; }'
      state.name = '韩梅梅'
    })

    return {
      state,
    }
  },
})
</script>

如上所示,新增了 state 中不存在的属性时,模板也能监听并渲染出 韩梅梅 的名字。但是此时ts会报错提示 state 不存在这个属性。

reactive方法还可以接收一个泛型参数,用来定义更复杂的类型。我们可以给 state 定义一个 name 属性可选的类型:

interface User {
  age: number
  name?: string
}
const state = reactive<User>({
  age: 18,
})

state.name = '韩梅梅' // 这里不再有报错了

ref

ref可以给复杂类型对象添加响应式,更常见的应用场景是给简单类型(字符串、数字、布尔值等)添加响应式

示例:

<template>
  <!-- vue会自动将模板中的ref的值展开,不应添加.value访问 -->
  <!-- 20 -->
  <p>{{ age }}</p>
  <!-- 韩梅梅 -->
  <p>{{ info.name }}</p>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const age = ref(18)
    const info = ref({
      name: '李雷',
      msg: '个人简介',
    })
    const tags = ref(['勤奋', '优秀'])

    onMounted(() => {
      // ref包裹的值需要使用.value进行访问
      age.value = 20
      info.value = {
        name: '韩梅梅',
        msg: '韩梅梅的个人简介',
      }
    })

    return {
      age,
      info,
      tags,
    }
  },
})
</script>

ref可以给原始值,需要添加一个 .value 后缀访问内部值。
但是在 template 模板中,ref会被自动展开,此时可以直接访问,无需追加 .value 的后缀。

TypeScript支持:

参考 类型声明 refs - 教程,ref与reactive一样,会默认将传入的值作为 .value 访问的内部值的声明:

const age = ref(18)

// Type 'string' is not assignable to type 'number'
age.value = '20'

同reactive,它也可以传入一个泛型参数作为内部值的声明:

const age = ref<number | string>(18)
age.value = '20' // 此处不再报错,因为内部值支持string类型

Computed - 计算属性

只读

接收一个 getter 函数,并为从 getter 返回的值返回一个 只读 的响应式 ref 对象:

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

twiceTheCounter.value++ // Error: 无法分配到 "value" ,因为它是只读属性。ts(2540)

可读写

它也可以使用具有 getset 函数的对象来创建 可读写 的计算属性:

import { ref, computed } from 'vue'

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

TypeScript支持

computed默认会将 getter 函数返回的值的类型作为默认类型:

const count = ref(1)

const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  },
})

plusOne.value = '1' // Error: 不能将类型“string”分配给类型“number”。ts(2322)

它也同样支持给 computed 方法传入一个泛型参数作为类型:

const count = ref<number | string>(1)

const testCount = computed<number | string>({
  get: () => count.value,
  set: val => {
    count.value = val
  },
})

testCount.value = '1'

这其实有些多此一举,还是建议在ref和reactive声明时就做好类型的声明。


Methods - 方法

在Composition API中创建一个方法非常简单,只需要在 setup 中声明一个函数,并导出,模板就能使用:

<template>
  <p>{{ age }}</p>

  <div @click="changeAge()">
    button
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const age = ref(18)

    // methods
    const changeAge = () => {
      age.value++
    }

    return {
      age,
      changeAge,
    }
  },
})
</script>

TypeScript方面也没什么特别值得注意的,这就是一个函数。


Watch - 侦听

Composition API中,有两种watch侦听:

  • watchEffect
  • watch

    watchEffect

    它立即执行传入的一个函数,同时响应式追踪其依赖。并在依赖改变时再次运行函数: ```typescript const count = ref(0)

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

setTimeout(() => { count.value++ // -> logs 1 }, 100)

与 `computed` 类似,它会自动检测要监听的依赖项。<br />watchEffect还有更多参数,想要详细的了解,可以访问 [watcheffect指南](https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html#watcheffect) 。
<a name="XRuih"></a>
## watch
数据源可以是返回值的 `getter` 函数,也可以直接是一个 `ref` :
```typescript
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (newVal, oldVal) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
  /* ... */
})

注意:当你需要监听一个深层嵌套对象或数组(无论是reactive还是ref)的property时,是不会触发事件的。

若要触发事件,则watch需要加上 deep 属性:

const refNumbers = ref([1, 2, 3, 4])
watch(
  () => refNumbers,
  (newVal, oldVal) => {
    console.log(newVal.value, oldVal.value)
  },
  { deep: true }
)
refNumbers.value.push(5) // logs: [1,2,3,4,5] [1,2,3,4,5]

const numbers = reactive([1, 2, 3, 4])
watch(
  () => numbers,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
  },
  { deep: true }
)
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4,5]

const user = reactive({ intro: { msg: '简介信息' } })
watch(
  () => user,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
  },
  { deep: true }
)
// logs: {intro: {msg: '新的简介信息'}} {intro: {msg: '新的简介信息'}}
user.intro.msg = '新的简介信息'

注意:仔细观察打印出来的信息,会发现watch监听到的对象/数组。 oldValnewVal 都是一样的,都指向我们监听的那个数据的最新的值。

因此当监听这类值的时候,需要传入一个由值构成的副本。为了能够完整的深度监听。可能需要传入一个深拷贝后的值。可以借助工具例如 lodash.cloneDeep 这样的实用工具来实现:

import _ from 'lodash'

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

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

state.attributes.name = 'Alex' // 日志: 'Alex' ''

一个侦听器还可以同时监听多组数据,想要更加详细的了解watch, 可以访问 watch 指南

TypeScript支持:
watch 也会默认将 getter 函数的返回值作为 第二个函数参数(newVal,oldVal) 的类型。

也可以给watch传入一个泛型参数,用来定义复杂类型:

watch<number | string>(
  () => state,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
  }
)

但不是很推荐这样子使用,建议还是在ref和reactive声明时做好类型声明。


Lifecycle Hooks - 生命周期钩子

这里将直接介绍在 setup 中调用生命周期,若想了解更多,请参考:

Vue3的生命周期有少许的改变:

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount

在 Composition API 当中,也会与 Options API 有细微的差别:

  • 不再需要 beforeCreatecreated 生命周期。setup 函数在 beforeCreate 之前调用。你可以理解为以前需要写在这两个钩子里的所有代码都直接写在 setup 函数里就行了。
  • 与Options API相比,所有钩子前都加了一个 on 前缀。
import {
  defineComponent,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onActivated,
  onDeactivated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered,
} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    console.log('not need beforeCreate and created')

    onBeforeMount(() => {})
    onMounted(() => {
      console.log('component mounted')
    })

    onBeforeUpdate(() => {})
    onUpdated(() => {})

    onActivated(() => {})
    onDeactivated(() => {})

    onBeforeUnmount(() => {})
    onUnmounted(() => {})

    onErrorCaptured(() => {})
    onRenderTracked(() => {})
    onRenderTriggered(() => {})
  },
})

生命周期还可以在封装的函数中调用!

import { defineComponent, onMounted } from 'vue'

const registerHooks = () => {
  onMounted(() => {
    console.log('component mounted')
  })
}

export default defineComponent({
  name: 'App',
  setup() {
    // 这样 mounted 也是生效的
    registerHooks()
  },
})

Ref - 模板引用

我们如何在 Composition API 中获取类似$refs的效果呢?也是使用 ref 函数。
其中原理和解释请参考:模板引用

示例:

<template>
  <div ref="root">test ref</div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    // 变量名就是模板ref属性的值
    const root = ref(null)

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

    // 要记得返回,ref才能生效
    return { root }
  },
})
</script>

TypeScript支持:

DOM引用

模板引用直接给 ref 函数泛型参数传参即可。这里需要注意由于 ref 中没有传入值,所以需要在声明时就给模板引用对象使用 类型断言 。或者在使用时使用 非空断言

<template>
  <input
    ref="inputEl"
    type="text"
  >
</template>

<script lang="ts">
import { defineComponent, ref, Ref, onMounted } from 'vue'
export default defineComponent({
  name: 'App',
  setup() {
    // 第一种:添加类型断言,告诉 ts 这就是一个input元素
    const inputEl = ref<HTMLInputElement>() as Ref<HTMLInputElement>

    onMounted(() => {
      // 第二种:添加非空断言,告诉 ts inputEl.value 绝对不为空
      console.log(inputEl.value!.value)
    })

    // 再次提醒,记得return出去
    return { inputEl }
  },
})
</script>

更多的元素内置类型,可以参考 Web APIs - MDN
在声明模板引用时就使用断言可能会比较方便,但一定记得要在 setupreturn 出去。

组件引用

当我们要使用 ref 引用一个组件,要如何使用呢?

<template>
  <HomeHeader ref="refHeader" />
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import HomeHeader from '@/components/Layout/HomeHeader.vue'

export default defineComponent({
  name: 'Home',
  components: { HomeHeader },
  setup() {
    const refHeader = ref<InstanceType<typeof HomeHeader>>()

    onMounted(() => {
      console.log(refHeader.value!.$data)
    })

    return { refHeader }
  },
})
</script>

这里使用到了一个 InstanceType ,这是一个 高级类型


Provide / Inject - 注入

provide 和 inject 用于祖孙组件传值,概念不赘述,详情可参考:

父组件使用 provide:

<!-- 父组件:src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { defineComponent, provide, ref, reactive } from 'vue'
import MyMarker from './MyMarker.vue'

export default defineComponent({
  components: {
    MyMarker,
  },
  setup() {
    provide('location', ref('North Pole'))

    provide(
      'geolocation',
      reactive({
        longitude: 90,
        latitude: 135,
      })
    )
  },
})
</script>

子组件使用 inject,可以传入默认值也可以不传:

<!-- 子组件:src/components/MyMarker.vue -->
<template>
  this is MyMarker.vue
</template>

<script lang="ts">
import { defineComponent, inject, ref, reactive, onMounted } from 'vue'

export default defineComponent({
  setup() {
    const location = inject('location', ref('North Pole'))

    const geolocation = inject('geolocation')

    onMounted(() => {
      console.log(location.value)
      console.log(geolocation.longitude)
    })

    return {
      location,
      geolocation,
    }
  },
})
</script>

建议在 provide 和 inject 中都使用 readonly 方法。
理由和具体操作可以参考 修改响应式 property - Provide / Inject - 教程

TypeScript支持

Inject 默认会将第二个参数传入的值的类型作为返回值的类型,当然也可以给泛型参数传值:

inject<Ref<string>>('location')
inject<{ longitude: number, latitude: number }>('refLocation')

Props

props用于接收来自父组件的数据,详细可参考:

  • Props - 教程
  • Props - API 参考
  • Props - Composition API

    示例:

    在上述的 setup 选项描述中,我们知道 setup 函数的第一个参数就是 props。而 setup 中的 props 的ts类型,会自动根据 props 选项的内容来推导值。 ```typescript import { defineComponent } from ‘vue’

export default defineComponent({ name: ‘HelloWord’, props: { userId: { type: [String, Number], default: () => 1, }, userInfo: { type: Object, default: () => ({ name: ‘’, age: 18, }), }, }, setup(props) { // (property) userId: string | number console.log(props.userId)

// (property) userInfo: Record<string, any>
console.log(props.userInfo.name)

}, })

上述代码发现,当我们定义一个对象或者需要其他复杂类型时,Vue提供的声明推导不好用了。`userInfo` 变成了一个带有 `any` key,`any` 值的对象。
<a name="Ky0CS"></a>
## TypeScript支持:
因此我们需要额外给Props进行ts注解,我们需要使用 `类型断言` 和 `PropType` 类型。<br />这块可详细参考:[注解 Props - 教程](https://v3.cn.vuejs.org/guide/typescript-support.html#%E6%B3%A8%E8%A7%A3-props)
```typescript
import { defineComponent, PropType } from 'vue'

interface UserInfo {
  id?: number | string
  name: string
  age: number
}

export default defineComponent({
  name: 'HelloWord',
  props: {
    userInfo: {
      type: Object as PropType<UserInfo>,
      default: () => ({
        name: '',
        age: 18,
      }),
    },
  },
  setup(props) {
    // (property) UserInfo.id?: string | number | undefined
    console.log(props.userInfo.id)

    // (property) UserInfo.name: string
    console.log(props.userInfo.name)
  },
})

Emits

在 Vue2 中,我们使用 自定义事件 时,是直接在组件使用的模板上定义,并直接使用 $emit 触发。
在 Vue3 中,事件的定义没有变化,但是新增了 emits 选项,用于声明在组件上已定义的事件。

在前面的 setup 章节中,我们已经了解到 emit 存在于 setup 函数的 context 参数中,想要了解更多,可以参考 emits - API参考

示例:

父组件给子组件定义事件:

<template>
  <div>我是父组件</div>
  <Child @toggle="toggleShow" />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Child from './components/Child.vue'

export default defineComponent({
  name: 'Parent',
  components: { Child },
  setup() {
    const toggleShow = (isShow: boolean) => {
      if (isShow) console.log('展示')
      else console.log('不展示')
    }

    return { toggleShow }
  },
})
</script>

子组件声明 & 触发事件:

<template>
  <div @click="emitEvent">
    我是子组件
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'Child',
  emits: ['toggle'],
  setup(_props, { emit }) {
    const isShow = ref(false)

    const emitEvent = () => {
      isShow.value = !isShow.value
      emit('toggle', isShow.value)
    }

    return { emitEvent }
  },
})
</script>

TypeScript支持:

emits 除了可以传入一个字符串组成的数组以外,还能传入对象,每个property就是一个事件,当 emits 传入的是对象时,就可以校验调用emit时传入的参数,了解更多可以参考 注解 emit - 教程

<template>
  <div @click="emitEvent">
    我是子组件
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'Child',
  emits: {
    toggle: (isShow: boolean) => {
      return typeof isShow === 'boolean'
    },
  },
  setup(_props, { emit }) {
    const isShow = ref(false)

    const emitEvent = () => {
      isShow.value = !isShow.value

      emit('toggle', isShow.value)

      // Error: Argument of type 'number' is not assignable to parameter of type 'boolean'.
      emit('toggle', 123)
    }

    return { emitEvent }
  },
})
</script>