@[TOC](自学前端开发 - VUE 框架 (四) 组合式 API)
vue2 中使用的是选项式API,到 vue3 中引入了组合式API。选项式API易于学习和使用,写代码的位置已经约定好了,但是代码组织性差,相似的逻辑代码不便于复用,逻辑复杂代码多了不好阅读。而组合式API在功能逻辑复杂繁多情况下,各个功能逻辑代码组织再一起,便于阅读和维护,缺点是需要有良好的代码组织能力和拆分逻辑能力。
在学习阶段,可以选择选项式API来写一些简单的程序。虽然 vue 对两种风格都支持,但是在一些 UI 框架或学习其他人写好的程序时,还是需要对两种风格都有些了解。另外虽然 vue 可以两种风格混用,但是在阅读、成员访问等方面会出现混乱,所以不推荐混用。
组合式API使用时也是基于应用实例,因此需要先创建一个应用实例。和选项式API不同的是,选项式API在创建应用实例时,传入的是若干选项组成的对象,而组合式API传入的,主要是 setup 函数对象。所有选项式API中各选项功能都写在 setup 函数中。
{{ num }}
setup 函数将返回一个对象,页面渲染需要的数据、方法、响应式状态需要放在此对象中。
在 setup 函数中手动暴露大量的状态和方法非常繁琐。当使用单文件组件(SFC)时,可以使用 来大幅度地简化代码。
选项式API提供的数据存放在 data、computed 等选项中,且会被创建为响应式数据。而组合式API则是在 setup 函数内使用响应式 api 来声明响应式状态。需注意的是,组合式api中只有使用响应式api声明的代理对象是响应式的,普通方式定义的数据是非响应式的。
reactive()
函数可以创建一个响应式对象或数组的代理:
const state = reactive({ count: 0 })
响应式转换是“深层”的:它会影响到所有嵌套的属性。
reactive 有两条限制:
let state = reactive({ count: 0 })// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
reactive()
的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref()
方法来允许我们创建可以使用任何值类型的响应式对象。
ref 方法接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
ref 对象是可更改的,也就是说你可以为 .value
赋予新的值。它也是响应式的,即所有对 .value
的操作都将被追踪,并且写操作会触发与之相关的副作用。
如果将一个对象赋值给 ref,那么这个对象将通过 reactive()
转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,可以使用 shallowRef()
来替代。
const count = ref(0)
console.log(count.value) // 0count.value++
console.log(count.value) // 1
简言之,ref()
让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到组合函数中。
当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value
。相反,如果在模板中使用了 .value
会获取不到数据。
需要注意的是,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。如果 ref 是文本插值(即一个 {{ }}
符号)计算的最终值,它也将被解包。
const object = { foo: ref(1) }
{{ object.foo + 1 }}
{ foo + 1 }}
-->
{ object.foo.value }} -->
{{ object.foo }}
相对于普通的 JavaScript 变量,我们不得不用相对繁琐的 .value
来获取 ref 的值。这是一个受限于 JavaScript 语言限制的缺点。然而,通过编译时转换,我们可以让编译器帮我们省去使用 .value
的麻烦。响应式语法糖是一个编译时的宏命令:它不是一个真实的、在运行时会调用的方法。而是用作 Vue 编译器的标记,表明最终的变量需要是一个响应式变量。
需要注意的是,响应式语法糖仍处于实验性阶段,有可能出现改动或其他问题。
一般对于普通数据类型,使用 ref()
,而对于数组和对象数据类型,则使用 reactive()
来声明(因为 reactive 声明不需要使用 .value
)
和选项式API中的 computed 选项类似,组合式API中可以使用 computed 函数创建计算属性。且也能够使用声明 get()
方法和 set()
方法的形式,创建可写的计算属性。
const count = ref(1)
const plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 2plusOne.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 函数。
实际上,组合式API没有 onCreated 这个钩子函数。因为 beforeCreated 和 created 这连个选项式的生命周期钩子在组合式API可以直接写在 setup()
函数中并执行,效果是一样的。
onMounted 钩子函数相当于 mounted 钩子选项。
等同于选项式API中的 updated
等同于选项式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 更新后执行 */
})
一般侦听器以同步方式创建,如果使用异步回调方式创建,那么它不会绑定到当前组件上,必须手动停止它,以防内存泄漏。
要手动停止一个侦听器,需要调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,尽可能选择同步创建。如果需要等待一些异步数据,可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)watchEffect(() => {if (data.value) {// 数据加载后执行某些操作...}
})