@[TOC](自学前端开发 - VUE 框架 (四) 组合式 API)
vue2 中使用的是选项式API,到 vue3 中引入了组合式API。选项式API易于学习和使用,写代码的位置已经约定好了,但是代码组织性差,相似的逻辑代码不便于复用,逻辑复杂代码多了不好阅读。而组合式API在功能逻辑复杂繁多情况下,各个功能逻辑代码组织再一起,便于阅读和维护,缺点是需要有良好的代码组织能力和拆分逻辑能力。
在学习阶段,可以选择选项式API来写一些简单的程序。虽然 vue 对两种风格都支持,但是在一些 UI 框架或学习其他人写好的程序时,还是需要对两种风格都有些了解。另外虽然 vue 可以两种风格混用,但是在阅读、成员访问等方面会出现混乱,所以不推荐混用。
基础用法
组合式API使用时也是基于应用实例,因此需要先创建一个应用实例。和选项式API不同的是,选项式API在创建应用实例时,传入的是若干选项组成的对象,而组合式API传入的,主要是 setup 函数对象。所有选项式API中各选项功能都写在 setup 函数中。
<div id="app">
<p @click="increment">{
{ num }}</p>
</div>
<script>
const {
createApp, ref} = Vue;
let app = createApp({
setup() {
// 响应式状态
let num = ref(0);
// 用来修改状态、触发更新的函数
function increment() {
num.value++
}
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
num, increment}
}
}).mount('#app')
</script>
setup 函数将返回一个对象,页面渲染需要的数据、方法、响应式状态需要放在此对象中。
在 setup 函数中手动暴露大量的状态和方法非常繁琐。当使用单文件组件(SFC)时,可以使用 <script setup>
来大幅度地简化代码。
响应式状态
选项式API提供的数据存放在 data、computed 等选项中,且会被创建为响应式数据。而组合式API则是在 setup 函数内使用响应式 api 来声明响应式状态。需注意的是,组合式api中只有使用响应式api声明的代理对象是响应式的,普通方式定义的数据是非响应式的。
reactive
reactive()
函数可以创建一个响应式对象或数组的代理:
const state = reactive({
count: 0 })
响应式转换是“深层”的:它会影响到所有嵌套的属性。
reactive 有两条限制:
- 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效。
- 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
let state = reactive({
count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({
count: 1 })
reactive()
的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref()
方法来允许我们创建可以使用任何值类型的响应式对象。
ref
ref 方法接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
ref 对象是可更改的,也就是说你可以为 .value
赋予新的值。它也是响应式的,即所有对 .value
的操作都将被追踪,并且写操作会触发与之相关的副作用。
如果将一个对象赋值给 ref,那么这个对象将通过 reactive()
转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,可以使用 shallowRef()
来替代。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
简言之,ref()
让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到组合函数中。
当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value
。相反,如果在模板中使用了 .value
会获取不到数据。
<script setup>
import {
ref } from 'vue'
const count = ref(0)
const object = ref({
a: 1, b: {
c: 1} })
function increment() {
count.value++;
object.value.a++;
object.value.b.c++;
}
</script>
<template>
<button @click="increment">
{
{ count }} <!-- 无需 .value -->
{
{ object.a }}
</button>
</template>
需要注意的是,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。如果 ref 是文本插值(即一个 {
{ }}
符号)计算的最终值,它也将被解包。
const object = {
foo: ref(1) }
<!-- 不会像预期一样工作,因为 object.foo 是一个 ref 对象,且不是顶层属性 -->
{
{ object.foo + 1 }}
<!--
可以通过将 foo 改成顶层属性来解决这个问题:
const { foo } = object
则可以使用
{
{ foo + 1 }}
-->
<!-- 会被解包,能够被渲染为1,相当于{
{ object.foo.value }} -->
{
{ object.foo }}
相对于普通的 JavaScript 变量,我们不得不用相对繁琐的 .value
来获取 ref 的值。这是一个受限于 JavaScript 语言限制的缺点。然而,通过编译时转换,我们可以让编译器帮我们省去使用 .value
的麻烦。响应式语法糖是一个编译时的宏命令:它不是一个真实的、在运行时会调用的方法。而是用作 Vue 编译器的标记,表明最终的变量需要是一个响应式变量。
<script setup>
let count = $ref(0)
console.log(count)
function increment() {
count++
}
</script>
<template>
<button @click="increment">{
{ count }}</button>
</template>
需要注意的是,响应式语法糖仍处于实验性阶段,有可能出现改动或其他问题。
一般对于普通数据类型,使用 ref()
,而对于数组和对象数据类型,则使用 reactive()
来声明(因为 reactive 声明不需要使用 .value
)
另外如果定义一个空的 ref 对象,然后在组件中绑定了这个对象,则可以通过此对象的 .value
属性获取组件对象实例。
computed
和选项式API中的 computed 选项类似,组合式API中可以使用 computed 函数创建计算属性。且也能够使用声明 get()
方法和 set()
方法的形式,创建可写的计算属性。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
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
方法
和选项式API中,方法必须写在 methods 选项中不同,组合式API可以在 setup()
函数内部的任意地方定义方法,且无需特殊标注。只要在返回对象中将需要外部使用的方法暴露出来即可。
生命周期钩子
组合式API的生命周期钩子不再是选项了,而是使用一些 api 函数。
onCreated
实际上,组合式API没有 onCreated 这个钩子函数。因为 beforeCreated 和 created 这连个选项式的生命周期钩子在组合式API可以直接写在 setup()
函数中并执行,效果是一样的。
onMounted
onMounted 钩子函数相当于 mounted 钩子选项。
<script setup>
import {
onMounted } from 'vue'
onMounted(() => {
console.log(`the component is now mounted.`)
})
</script>
onUpdated
等同于选项式API中的 updated
<script setup>
import {
ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
// 文本内容应该与当前的 `count.value` 一致
console.log(document.getElementById('count').textContent)
})
</script>
<template>
<button id="count" @click="count++">{
{ count }}</button>
</template>
onUnmounted
等同于选项式API中的 unmounted。和选项式API中几乎不会用到不同,组合式API可能会用在一些需要显式手动释放资源的情况中,例如异步侦听器不会绑定到当前组件,所以需要手动停止。
侦听器
组合式API的侦听器类似于选项式API的侦听器,但还是有区别的。主要在侦听的数据源类型
基本使用
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX,oldX) => {
console.log(`x is ${
newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum, oldSum) => {
console.log(`sum of x + y is: ${
sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${
newX} and y is ${
newY}`)
})
多个数据源
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
对于响应式对象
目前,对于 reactive()
声明的响应式对象,侦听器不能正确的获取 oldValue,且侦听器是深度侦听,deep 选项失效。
对于 reactive()
声明的响应式对象的属性,不能直接侦听
const obj = reactive({
count: 0 })
// 错误,因为 watch() 得到的参数 obj.count 是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${
count}`)
})
而正确的方法是需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${
count}`)
}
)
此种使用 getter 函数的侦听器是浅层侦听器,如果需要将这种侦听器转换为深层侦听器,则需要显式地加上 deep 选项:
let a = {
b: {
c: 1}}
watch(() => a.b, (newValue, oldValue) => {
// 因为侦听器是浅层侦听
// 无法获取 c 的状态改变
})
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{
deep: true }
)
即时回调
同选项式API,侦听器是懒执行的。在组合式API中,可以显式调用侦听函数来立即执行。
const url = ref('https://...')
const data = ref(null)
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 立即获取
fetchData()
// ...再侦听 url 变化
watch(url, fetchData)
也可以用 watchEffect 函数 来简化上面的代码。watchEffect()
会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。上面的例子可以重写为:
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
回调的触发时机
同选项式API,侦听器会在DOM更新前调用,也可以添加选项使得侦听器在DOM更新后调用
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
import {
watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
停止侦听器
一般侦听器以同步方式创建,如果使用异步回调方式创建,那么它不会绑定到当前组件上,必须手动停止它,以防内存泄漏。
<script setup>
import {
watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {
})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {
})
}, 100)
</script>
要手动停止一个侦听器,需要调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {
})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,尽可能选择同步创建。如果需要等待一些异步数据,可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
模板引用
在使用组合式API时,对于访问模板引用方式除了使用 Vue 对象的 $refs
属性外,还可以暴露一个同名的空 Ref()
对象,通过此对象进行访问。
使用 $refs
属性
$refs
属性实际上还是选项式 API 的方式,只是在组合式API还能够使用。不同的是组合式 API 没有了 this
,所以使用时只能通过 Vue 对象实例使用了。
但是此方法的缺点却很明显:在 Vue 对象没有初始化完成,是获取不到 Vue 对象实例的。所以在 onMonted 等生命周期钩子中无法使用,只能在一些触发事件中使用。
优点也很明显:无需定义和 return 数据,只需要在元素上设置 ref
属性就可以直接访问模板引用。
使用空 Ref
对象
定义一个同名的空 Ref
对象进行访问
const input = ref();
const table = ref();
onMounted(() => {
input.value.focus();
table.value.clearSort();
})
注意使用对象的 value
属性才能正确访问其引用。注意需要将 Ref
对象 return
使用数组
当在 v-for
中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
<script setup>
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{
{ item }}
</li>
</ul>
</template>
注意单个访问时,使用的是 itemRefs.value[index]
而不是 itemRefs[index].value
。另外 ref 数组并不保证与源数组相同的顺序。
函数模板引用
除了使用字符串值作名字,ref 属性还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="el => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
这样就能够在使用数组时保证顺序正确了
<li v-for="(item, index) in list" :ref="el => itemRefs[index] = el">
{
{ item }}
</li>
需要注意的是使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。也可以绑定一个组件方法而不是内联函数。
另外,绑定 ref 属性的值可以没有经过定义就进行使用。实际上会将此值作为 this.$refs
这个对象的 key 来保存引用。而使用 :ref
则必须使用函数模板引用,而函数内部使用的变量必须是定义并暴露的,否则将无法正常访问引用。
使用对象
使用函数也可以将模板引用赋值给对象的成员,这样可以将若干组件来分类
<script setup>
const inputRefs = ref({
})
onMounted(() => console.log(inputRefs.value['address']))
</script>
<template>
<input name='address' :ref="el => inputRefs['address'] = el" />
<input name='age' :ref="el => inputRefs['age'] = el" />
</template>