Composition API(二):常用的 Composition API

128 阅读8分钟

在 Vue3 中,默认情况下定义的数据都不是响应式的,所以需要我们手动的去将它们添加到响应式中,所以提供了 ReactiveRef 等 API

如下代码,使用 setup 函数实现点击按钮 +1 的功能:

<template>
  <div>
    <h4>{{ age }}</h4>
    <button @click="increment">+1</button>
  </div>
</template>
export default {
  setup() {
    let age = 20

    let increment = () => {
      age++
      console.log(age) // 每次点击 age 的值都会 +1
    }

    return {
      age,
      increment
    }
  }
}

这时我们会看到,age 的值确实发生了改变,但是页面上的 age 并没有随之改变,这是因为在 setup 函数中直接定义的数据不是响应式的,这时我们可以使用 reactive 函数来定义。

Reactive :使对象本身具有响应性

<template>
  <div>
    <h4>{{ state.age }}</h4>
    <button @click="increment">+1</button>
  </div>
</template>
import { reactive } from 'vue'

export default {
  setup() {
    // 使用 reactive 函数来定义变量
    const state = reactive({
      age: 20
    })

    let increment = () => {
      state.age++
      console.log(state.age)
    }

    return {
      state, // 对应的应该返回 state
      increment
    }
  }
}

注意,reactive 函数一般用来将对象或数组变成响应式的,所以如果是简单数据类型,则必须放在一个对象或者数组中才可以,就像上面一样。但是通常我们会使用 Ref API 来处理简单数据类型。

reactive 函数的返回值是对应已经被 代理 的对象或数组。

为什么使用 reactive 函数来定义就可以变成响应式的呢?

这是因为当我们使用 reactive 函数处理我们的数据之后,数据再次被使用时就会进行依赖收集,当数据发生改变时,所有收集到的依赖都会进行对应的响应式操作(比如更新界面),事实上,我们编写的 data 选项,也是在内部交给了 reactive 函数将其变成响应式对象的。

reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。

Ref:将内部值包装在特殊对象中

Reactive API 对传入的类型是有限制的,它要求我们必须传入的是一个 对象 或者 数组 类型,如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告。

这个时候 Vue3 给我们提供了另外一个API:ref API。

ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值。它内部的值是在 ref 的 value 属性中被维护的。

也就是说,当我们用 ref 对数据进行处理后,它会返回一个响应式的对象,如果我们想要获取到该数据的值,则需要通过该 ref 对象的 value 属性来获取。

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到。

<template>
  <div>
    <!-- age 是一个 ref 对象,一般来说不能直接使用,但是在模板中使用 ref 对象时会自动解包 -->
    <h4>{{ age }}</h4>
    <button @click="increment">+1</button>
  </div>
</template>
import { ref } from 'vue'

export default {
  setup() {
    // age 是一个 ref 对象
    let age = ref(20)

    let increment = () => {
      // age 是一个 ref 对象,不能直接使用,要通过它的 value 属性来获取它的值。
      age.value++
      console.log(age.value)
    }

    return {
      age,
      increment
    }
  }
}

这里有两个注意事项:

在模板中引入 ref 的值时,Vue 会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式 来使用

但是在 setup 函数内部,它依然是一个 ref 引用, 所以对其进行操作时,我们依然需要使用 ref.value 的方式。

readonly

toRefs:对某个响应式的对象进行解构时使用

如下案例:

<template>
  <div>
    <h4>{{ state.name }}-- {{ state.age }}</h4>
    <button @click="increment">+1</button>
  </div>
</template>
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      name: 'conan',
      age: 20
    })

    let increment = () => {
      state.age++
      console.log(state.age)
    }

    return {
      state,
      increment
    }
  }
}

如果我们希望对 reactive 返回的对象进行解构赋值,则代码修改如下:

<template>
  <div>
    <h4>{{ name }}-- {{ age }}</h4>
    <button @click="increment">+1</button>
  </div>
</template>
  setup() {
    const state = reactive({
      name: 'conan',
      age: 20
    })
    
    let { name, age } = state
  }

这时不管是修改解构后的变量:

    let increment = () => {
      age++
    }

还是修改 reactive 返回的 state 对象:

    let increment = () => {
      state.age++
    }

数据都不再是响应式的。

所以 Vue 为我们提供了一个 toRefs 函数,可以将 reactive 返回的对象中的属性都转成 ref,即响应式的数据。

import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const state = reactive({
      name: 'conan',
      age: 20
    })
    let { name, age } = toRefs(state)
    let increment = () => {
      age.value++
      // state.age++  也可以
      console.log(state.age)  // 也会随之改变
    }

    return {
      name,
      age,
      increment
    }
  }
}

注意,这里当我们修改解构出来的变量时,state里的属性值也会随之改变,反之也是。也就是 toRefs 相当于在 state.nameref.value 之间建立了链接,任何一个修改都会引起另外一个变化。

toRef

computed:计算属性

<template>
  <div>
    <h3>{{ fullName }}</h3>
    <button @click="changeLastName">修改 lastName</button>
  </div>
</template>
import { ref } from 'vue'

export default {
  setup() {
    const firstName = ref('detective')
    const lastName = ref('conan')
    const fullName = firstName.value + ' ' + lastName.value

    const changeLastName = () => {
      lastName.value = 'caohan'
      console.log(fullName) // detective conan
    }

    return {
      fullName,
      changeLastName
    }
  }
}

这时当我们点击按钮修改 lastName 时, fullName 的值不会更改,页面的视图也不会改变,所以这时的 fullName 不是响应式的。

当然我们可以直接使用 firstName 拼接 lastName,因为这两个变量都是响应式的,但是当我们在多个地方都需要使用 firstName 拼接上 lastName 时,使用 fullName 无疑是一个更好的选择。

那么这个时候怎么办呢,有没有什么办法使得 fullName 变成响应式的呢?

这时我们就可以使用 computed 方法。

为什么使用 computed 方法就会变成响应式的呢,是因为 fullName 是依赖 firstNamelastName,当这两个变量发生改变时,就会重新调用 computed 函数的 get 方法

computed 方法可以接受一个函数(get 函数)作为参数:

import { ref, computed } from 'vue'

export default {
  setup() {
    const firstName = ref('detective')
    const lastName = ref('conan')
    
    const fullName = computed(() => {
      return firstName.value + ' ' + lastName.value
    })

    const changeLastName = () => {
      lastName.value = 'caohan'
      console.log(lastName.value)
    }

    return {
      fullName,
      changeLastName
    }
  }
}

也可以接受一个对象作为参数,对象包含 get 和 set 方法:

import { ref, computed } from 'vue'

export default {
  setup() {
    const firstName = ref('detective')
    const lastName = ref('conan')
    
    const fullName = computed({
      get() {
        return firstName.value + ' ' + lastName.value
      },
      set(newValue) {}
    })

    const changeLastName = () => {
      lastName.value = 'caohan'
      console.log(lastName.value)
    }

    return {
      firstName,
      lastName,
      fullName,
      changeLastName
    }
  }
}

computed 方法会返回一个 ref 对象

watchEffect

watchEffect的执行时机

setup 中使用 ref 引用

在 vue2 中如果我们想要获取到某个组件或者是某个元素,可以通过 ref 引用的方式来获取:

<template>
  <div>
    <h3 ref="title">hellow world</h3>
  </div>
</template>
 export default {
  mounted() {
    this.fn()
  },
  methods: {
    fn() {
      console.log(this.$refs.title.innerHTML) // hellow world
    }
  }
}

但是在 vue3 中,由于没有绑定 this,所以不能通过这种方式来获取。我们只需要定义一个 ref 对象,绑定到元素或者组件的 ref 属性上即可:

<template>
  <div>
    <!-- 绑定到元素或者组件的 ref 属性上 -->
    <h3 ref="title">hellow world</h3>
  </div>
</template>
import { ref, watchEffect } from 'vue'

export default {
  setup() {
    // 定义一个 ref 对象
    const title = ref(null)
    
    watchEffect(() => {
      console.log(title.value)
    })
    
    return {
      title
    }
  }
}

watchEffect 的副作用函数会在 DOM 挂载前默认执行一次,如果我们在副作用函数中有需要操作 DOM 的逻辑,那么第一次执行时会获取不到,所以我们可以将副作用函数的执行时机设置为 DOM 更新后:

   watchEffect(
      () => {
        console.log(title.value) // <h3>hellow world</h3>
      },
      {
        flush: 'post'
      }
    )

watch

watch 函数侦听的数据源有两种类型:

  • 可响应式的对象,reactive 或者 ref(比较常用的是 ref)
  • getter 函数,但是该 getter 函数必须引用可响应式的对象(比如 reactive 或者 ref)

情况一:监听一个 reactive 对象(监听数组或对象时使用)

import { reactive, watch } from 'vue'

export default {
  setup() {
    const info = reactive({
      name: 'conan',
      age: 20
    })

    watch(info, (newVal, oldVal) => {
      // 获取到的 newVal 和 oldVal 也是 reactive 对象
      console.log(newVal, oldVal)
    })

    const changeValue = () => {
      // 因为源码中开启了深度监听,所以 info 的 name 值改变可以触发监听
      info.name = 'kobe'
    }

    return {
      info,
      changeValue
    }
  }
}

对一个 reactive 对象进行监听时,获取到的变化前后的值也是 reactive 对象。(如果想获取普通对象或数组,方法见后。)

情况二:监听一个 ref 对象(一般来说都是监听简单数据类型时使用)

1. ref 对象的值是一个简单类型数据:

import { ref, watch } from 'vue'

export default {
  setup() {
    const name = ref('abc')

    watch(name, (newVal, oldVal) => {
      // 获取到的 newVal 和 oldVal 是 value 值
      console.log(newVal, oldVal) // kobe abc
    })

    const changeValue = () => {
      name.value = 'kobe'
    }

    return {
      name,
      changeValue
    }
  }
}

对一个 ref 对象进行监听时,获取到的变化前后的值是对应的 value 值。

2. ref 对象的值是一个对象,那么只有在改变该对象的指向时才会触发监听:

  setup() {
    const title = ref({
      name: 'conan',
      age: 20
    })

    watch(title, (newVal, oldVal) => {
      console.log(newVal, oldVal) // kobe Proxy{}
    })

    const changeValue = () => {
      title.value.name = 'kobe' // 不会触发监听,因为源码中对 ref 对象没有开启深度监听。
      title.value = 'kobe' // 会触发监听
    }

    return {
      title,
      changeValue
    }
  }

情况三:监听一个 getter 函数(一般用来监听某个 reactive 对象或 ref 对象的具体属性)

import { ref, reactive, watch } from 'vue'

export default {
  setup() {
    const info = reactive({
      name: 'conan',
      age: 20
    })
    
    // const info = ref({
    //   name: 'conan',
    //   age: 20
    // })
    
    watch(
      () => {
        return info.name
        // return info.value.name
      },
      (newVal, oldVal) => {
        console.log(newVal, oldVal)
      }
    )

    const changeValue = () => {
      info.name = 'kobe'
      // info.value.name = 'kobe'
    }

    return {
      info,
      changeValue
    }
  }
}

补充:

当我们在监听一个 reactive 对象时,获取到的变化前后的值也是 reactive 对象。如果我们希望获取到的值是一个普通对象或数组,可以通过监听 getter 函数的方式来处理:

export default {
  setup() {
    const info = reactive({
      name: 'conan',
      age: 20
    })
    
    watch(
      () => {
        return { ...info }
      },
      (newVal, oldVal) => {
        console.log(newVal, oldVal)
      }
    )

    const changeValue = () => {
      info.name = 'kobe'
    }

    return {
      info,
      changeValue
    }
  }
}

当用这种方式来处理时,默认的深度监听就不会生效,因为深度监听是只针对 reactive 对象的,解构之后就只是普通的对象或数组。可以设置 watch 方法的第三个参数来实现深度监听和立即执行。

总结:

  • 当我们需要监听某个简单类型的数据时,一般监听 ref 对象
  • 当我们需要监听某个对象或数组时,一般监听 reactive 对象
  • 当我们需要监听对象的某个属性时,一般监听 getter 函数

侦听多个数据