响应式api

响应式api相关知识复习。

ref()

这个api会返回一个代理对象,通过.value去获取值。该api的设计主要是为了弥补reactive()
无法代理数字、字符串类型。当传入一个对象的时候,ref内部实际上调用的是reactive()这个api。

  • ref嵌套场景
1
2
3
4
5
6
7
8
9
import {ref, shallowRef} from "vue"

// 使用 ref
const deepRef = ref({a: ref({b: 2}), c: "c"});
console.log(deepRef.value.a.b); // 输出 2

// 使用 shallowRef
const shallow = shallowRef({a: ref({b: 2}), c: "c"});
console.log(shallow.value.a.value.b); // 输出 2

当存在嵌套的时候,使用vue会自动把内层的ref进行解包,然后把其转换成响应式的。所以deepRef的定义相当于这么定义

1
const deepRef = ref({a: {b: 2}, c: "c"})

shallowRef 只会对其顶层属性进行响应式转换,而不会对嵌套的对象进行深层次的响应式处理。

computed()

当我们需要使用已有的状态去去描述其他的状态的时候,通常使用该api。

该api会自动处理计算状态与已有状态的依赖关系。

官方给出的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 只读
function computed<T>(
getter: (oldValue: T | undefined) => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
options: {
get: (oldValue: T | undefined) => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {computed, ref} from "vue"

const a = ref(10)

const b = computed(() => {
console.log("computed")
setTimeout(() => {
console.log("time out")
return a.value--
}, 2000)
return a.value + 10
})

console.log(b.value)
console.log(b.value)
a.value = 8
setTimeout(() => {
console.log(b.value)
}, 3000)

/*
computed
20
20
time out
computed
20
time out
*/

再computed中,getter回调中应该只做相关属性的计算,不应该产生其他的副作用,比如 修改dom做异步请求 等。从上面的例子可以很好的体会到。
因为这个api的设计理念就是提供给我们做实时性的数据计算的。并且计算出来的值再getter函数执行的时候就已经被确定了
,定时器里的内容会被放到宏队列中等待。
等待2s之后,a的值++。 所以一般不要在getter中做产生副作用的操作。一般会使用侦听器watch watchEffct去做。

第二个参数可以设置一个setter回调,这样意味着这个计算属性是可以更改的,如果没有设置setter回调,当我们去更改的时候,控制台会报一个
[Vue warn] Write operation failed: computed value is readonly 的警告
并且vue并不会去更改它的值。

  • 使用
1
2
3
4
5
6
7
8
9
10
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

reactive()

reactive()会返回一个对象的响应式代理,和ref()差不多,但是有部分的点是不一样的。
比如对于数组、Map 这样的对象,响应式ref嵌套的时候,ref()会自动解包,但是reactive不会。reactive不能代理基本类型(
数字、字符串等)

1
2
3
4
5
import {reactive, ref} from "vue"

const arr = reactive([ref({a: 1}), 2, 3, 4])

console.log(arr[0].value.a) // 1

当我们强制使用reactive()去代理一个基本类型的时候,reactive()不会给我们返回一个响应式对象,而是将其原原本本的返回回来,并且会在控制台中输出警告。

1
2
3
4
5
6
7
import {isReactive, reactive, ref} from "vue"

const a = "strong"
let b = reactive(a)
console.log(isReactive(b))
// [Vue warn] value cannot be made reactive: strong
// false

避免深层次的响应式转换,与ref(),类似,也有相同的浅层转换api,shallowReactive(),不过多赘述。

readonly()

这个api将接收一个对象,可以是响应式的或者是普通的原生对象,会返回一个只读响应式副本。

1
2
3
4
5
6
7
8
9
10
11
12
import {isReadonly, readonly} from "vue";

const a = {s: "ssss"}
const b = readonly(a)

console.log(b)

a['number'] = 12
console.log(isReadonly(b), b)

// { s: 'ssss' }
// true { s: 'ssss', number: 12 }

watchEffect

这个api接收一个副作用函数,这个副作用函数会立即执行一次,watchEffect()会自动跟踪相关的依赖项,并且当依赖项状态变化的时候,会重新执行

1
2
3
4
5
6
7
8
import {watchEffect, ref} from "vue";

const a = ref(0)

watchEffect(() => {
console.log(a.value, "watchEffect")
})
a.value++

副作用函数也可以接收一个参数,这个参数是一个函数类型,当依赖项变化的时候才会执行该函数,这个函数的参数也是一个函数类型。这里稍微有点绕
但是通过官方给出的类型定义就可以理解了

1
2
3
4
5
6
function watchEffect(
effect: (onCleanup: OnCleanup) => void,
options?: WatchEffectOptions
): StopHandle

type OnCleanup = (cleanupFn: () => void) => void
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {watchEffect, ref} from "vue";

const a = ref(0)

function func() {
console.log(666)
}

watchEffect((onCleanup) => {
onCleanup(func)
console.log(a.value, "watchEffect")
})
a.value++
console.log("啊啊啊")

// 0 watchEffect
// 啊啊啊
// 666
// 1 watchEffect

必须要明确一点的是,watch系列的api都是异步的,这样说还是比较模糊,也就是当依赖项改变的时候,会产生一个微队列任务。所以当
a.value++ 的时候,并不会立即执行副作用函数,而是先去输出 啊啊啊

当不再需要这个侦听器的时候,可以通过watchEffect()的返回值将它销毁掉。一般不怎么会使用到

1
2
3
4
5
const stop = watchEffect(() => {
})

// 当不再需要此侦听器时:
stop()

watch()

该api可以侦听一个或者多个响应式状态,并且在状态变化的时候执行回调函数。
也就是说,可以指定侦听的数据源,相较于watchEffect()来说,在代码上比较直观,因为可以在传递参数部分就可以知道监听的是哪个状态。
并且可以在回调函数中访问到变化之前的值同样的,当数据变化的时候会创建一个微队列任务。

区别于watchEffect(),回调函数不会立即执行,只会在所依赖的状态改变的时候才会执行

  • 使用
1
2
3
4
5
6
7
8
9
10
11
import {reactive, watch} from "vue";

const state = reactive({num: 1})

watch(() => state.num, (newValue, oldValue) => {
console.log(newValue, "new")
console.log(oldValue, "old")
})
state.num++
// 2 new
// 1 old

第一个参数的类型可以是以下几种类型

  • 一个函数,这个函数要返回一个值
  • 一个ref
  • 一个响应式对象
  • 由以上类型的值组成的数组

但是有时候总会有一些奇奇怪怪的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {reactive, ref, watch} from "vue";

const state = reactive({num: 1, age: 15});

watch(
() => state,
(newValue, oldValue) => {
console.log("New Value:", newValue);
console.log("Old Value:", oldValue);
}
);

// 修改属性以触发监听器
state.num++;

这样写回调里面的内容是不会触发的,因虽然传递了一个getter()
函数,但是vue不知道你要监控的是这个对象里面的哪一个属性,所以这样写是感知不到的。但是直接把这个回调去掉又会出现一个奇奇怪怪的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {reactive, ref, watch} from "vue";

const state = reactive({num: 1, age: 15});

watch(
state,
(newValue, oldValue) => {
console.log("New Value:", newValue);
console.log("Old Value:", oldValue);
}
);

// 修改属性以触发监听器
state.num++;

// New Value: { num: 2, age: 15 }
// Old Value: { num: 2, age: 15 }

函数是执行了,但是输出的东西似乎和直觉上不一样,不应该是这样的吗?

New Value: { num: 2, age: 15 }
Old Value: { num: 1, age: 15 }

在内部处理的时候,因为监控的是一个对象,newValue和oldValue实际上指向的是同一个引用,在回调中加上这一行代码

console.log(newValue === oldValue)

你会惊奇的发现输出的结果是true,这就证明了newValue与oldValue指向的是同一个引用。在开发中要稍微注意一下。