自学前端开发 - VUE 框架 (四) 组合式 API

@[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 有两条限制:

  1. 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效。
  2. 因为 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>

猜你喜欢

转载自blog.csdn.net/runsong911/article/details/127867128
今日推荐