在 Vue3 中,默认情况下定义的数据都不是响应式的,所以需要我们手动的去将它们添加到响应式中,所以提供了 Reactive、Ref 等 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.name 和 ref.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 是依赖 firstName 和 lastName,当这两个变量发生改变时,就会重新调用 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 函数