本文代码都是在 单文件组件 的基础上使用
前言
Vue3 对 TypeScript 提供了更好的支持,大部分内容可以不用显示定义类型,就像是在写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或与之无关,使用时还是直接声明在选项当中:
- name - 组件名
- components - 引入的子组件
- directives - 自定义指令
- mixins - 混入
- …
```vue
---
<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 中,要创建一个响应式的值,需要用到 ref
和 reactive
两个方法。
若想详细的了解这两个方法,可以参考:
本篇不说明用哪种会更好,不同应用场景下有不同的使用方法,最好是ref和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)
可读写
它也可以使用具有 get
和 set
函数的对象来创建 可读写 的计算属性:
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监听到的对象/数组。 oldVal
和 newVal
都是一样的,都指向我们监听的那个数据的最新的值。
因此当监听这类值的时候,需要传入一个由值构成的副本。为了能够完整的深度监听。可能需要传入一个深拷贝后的值。可以借助工具例如 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 有细微的差别:
- 不再需要
beforeCreate
和created
生命周期。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 。
在声明模板引用时就使用断言可能会比较方便,但一定记得要在 setup
内 return
出去。
组件引用
当我们要使用 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 / Inject - 教程
- Provide / Inject - Composition API
- Provide / Inject - API参考
基础用法
provide 传入两个参数,第一个是注入的key,第二个则是注入的值。
为了保证数据能够响应式变更,provide 传入的值应该是个ref
或reactive
包裹的值。
父组件使用 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>