前言

本文主要整理关于在vue3中所提及到的所有与ref相关的API,通过对比每个API的作用以及使用场景,新增对vue3中相关的API的认知,主要借助于vue3官方API的阅读!

“ref”成员一览

参考官方所整理的关于不同场景下的“ref”,对应整理 👇 的相关属性

响应式核心:ref()

ref接收一个内部值,返回一个响应式的,可更改的ref对象,此对象只有一个指向其内部值的属性.value
关于该函数的签名以及对象的类型定义如下:

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
31
32
33
// packages/activity/src/ref.ts
// UnwrapRefSimple类型用于标识取消包装后的简单类型
export type UnwrapRef<T> = T extends ShallowRef<infer V> ? V : T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>
// 通过重载的方式来定义这个ref()函数
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if(isRef(rawValue)){
return rawValue
}
return RefImpl(rawValue, shallow)
}
class RefImpl<T>{
private _value: T
private _rawValue: T
public readonly __v_isRef = true // 这对于后续用来判断一个对象是否为一个ref对象非常有用
get value() {
// 设置监听
trackRefValue(this)
return this.value
}
set value() {
if(已经改变){
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发更新
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

🌟 上述我们发现关于ref()方法定义了三个重载,目的是兼容多种不同的调用方式,可以根据传入的参数来确定调用哪个函数签名,上述三种重载分别描述如下:

  1. 接收一个参数value,并返回一个Ref类型的实例,一般适用于初始化一个具有初始值的响应式变量;
  2. 不接受任何参数,返回一个Ref类型的实例,一般适用于初始化一个没有初始值的响应式变量;
  3. 最终具体函数实现,根据传入的参数来调用不同的重载,从而实现了对不同的调用方式的兼容性。

👊 注意我们上述对这个响应结果进行加粗处理,需要关注下这里就算是传递的基本数据类型的value,其响应结果都是一个实例对象,这里我们可以发现当调用ref()的时候,最终返回的都是一个new RefImpl<T>()实例对象,该对象将传递进入来的value作为其_value属性来存储,并针对value提供了对应的getter以及setter函数,当我们针对ref()的响应结果的value属性进行访问时,都将会触发到相应的“订阅通知操作”(通过上述的trackRefValue()以及triggerRefValue()!

响应式工具:isRef()、unref()、toRef()、toRefs()

isRef()

检查某个值是否为ref对象,此方法的定义如下:

1
2
3
4
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref<T>{
return !!(r && r.__v_isRef === true)
}

🌟 上述这里通过判断一个r对象是否拥有__v_isRef属性,从而来判断这个r对象是否为一个ref实例对象,具体可上 👆 的关于class RefImpl的定义!

unref()

如果参数是ref对象,则返回其内部值value,否则返回参数本身,此方法的定义如下:

1
2
3
export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T{
return isRef(ref) ? ref.value : ref
}

🌟 通过借助于isRef()方法,用来将一个”类似于Ref”的对象给返回其value属性!

toRef()

可以将值、refs或者getters规范化为refs,也可以针对一个ref对象中的内部值value的属性来创建一个对应的ref对象(这里我愿称之为value子属性ref对象),这样子就可以让这个子属性ref对象与父ref对象中的value保持同步,一旦有一方发生改变,另外一方也相应地发生改变,关于此方法的定义(3个不同的重载方法)如下:

1
export function toRef<T>(value: T): T extends () => infer R ? Readonly<Ref<R>> ? T extends Ref ? T : Ref<UnwrapRef<T>>

该泛型函数接收一个参数value,根据参数的类型来确定返回值的类型,如果value是一个函数类型,则返回一个只读的Ref类型的实例;如果value是一个Ref类型的实例,则直接返回这个ref实例;否则直接将这个value包装起来的Ref对象实例!

1
2
//注意这里下方的返回值ToRef(),这个是vue3中所提供的一个类型工具函数,代表这个类型是一个Ref类型
export function toRef<T extends Object, K extends keyof T>(object: T, key: K): ToRef(T[K])

该泛型函数接收两个参数object与key,其中object是一个对象,key是对象T的属性名,返回一个ToRef<T[K]>类型的值,表示对象object的属性key的响应式引用,主要将对象的某个属性给ref相应化,并保持与源对象的属性的数据同步!

1
2
3
4
5
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>

该泛型函数接收两个参数object和key以及一个可选参数defaultValue,与上面第二个类似,但补充了一个逻辑:如果属性值为undefined,则使用defaultValue来代替!

👊 下面是最终的该函数的实现

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
31
32
33
34
35
36
37
38
39
export function toRef(
source: Record<string, any> | MaybeRef,
key?: string,
defaultValue?: unkndown
): Ref{
if(isRef(source)){
return source
}else if(isFunction(source)){
return new GetterRefImpl(source) as any // 这里代表拿function的执行结果来作为返回值
}else if(isObject(source) && arguments.length > 1){
// 除了传递object,还传递了key属性
return propertyToRef(object, key!, defaultValue)
}else{
return ref(source)
}
}
// 将一个对象中的属性转化为ref对象,并与原始属性建立同步连接
function propertyToRef(source: Record<string, any>, key: string, defaultValue){
const val = source[key]
return isRef(val) ? source[key] : (new ObjectRefImpl(source, key, defaultValue) as any)
}
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K],
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue! : val
}
set value(newVal) {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}

关于这个方法API的具体使用以及需要注意的地方,详见官方toref

toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的ref对象,每个单独的属性ref对象都是使用toRef()来创建的,该函数的定义如下

1
2
3
4
5
6
7
8
9
export type ToRefs<T = any> = {
[K in keyof T]: toRef<T[K]>
}
export function toRefs<T extends object>(object: T): ToRefs<T>{
const ret: any = isArray(object) ? new Array[object.length] : {}
for(const key in object){
ret[key] = propertyToRef(object, key)
}
}

注意这里我们通过toRefs()方法所创建出来的对象是一个普通的对象,其属性成员才是ref对象!

响应式进阶:shallowRef()、triggerRef()、customRef()

shallowRef(): 对value的直接改变才触发更新

ref()的浅层响应作用形式。通常情况下,使用ref()函数所创建的响应式饮用会对其值进行深层响应式转换,即时值是一个对象,其内部的属性也会被转换为响应式的, 👉 这意味着当对象的属性发生变化时,视图会自动更新。但是有时,我们又不想它去自动更新,因此可以采用shallowRef,它创建的响应式引用时浅层的,即只对其值进行浅层响应式转换,也就是说对.value的访问时响应式的, 🤌 这通常对于大型数据结构的性能优化或者时外部状态管理系统集成非常有用!
以下是关于该API的简单运用:

1
2
3
const state = shallowRef({ count: 1 })
state.value.count = 2 // 这将不会触发改变
state.value = { count: 2 } // 这将会触发改变

关于该方法的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function shallowRef<T>(
value: T,
): Ref extends T
? T extends Ref
? IfAny<T, ShallowRef<T>, T>
: ShallowRef<T>
: ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
return createRef(value, true)
}

function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

🌟 从上面我可以看到该方法与ref()使用相差无异,就只是其底层在new RefImpl()的时候,传递了属性shallow=true

triggerRef()

强制触发依赖于一个浅层ref: 对value的直接改变才触发更新)的副作用,一般在对浅引用的内部值进行深度变更后使用,一般情况下,vue3中响应式引用的更新是由其自身的响应式系统自动管理的,当引用的值发生变化时,视图自动更新, 但有时候,我们可能需要手动触发更新,这时就可以使用triggerRef函数

1
2
3
4
5
6
7
8
9
10
11
const shallow = shallowRef({
foo: 'hello vue3'
})
// 通过watchEffect来监听,一旦响应式引用改变就输出其值
watchEffect(() => {
console.info(shallow.value.foo)
})
// 这里的赋值,将不会自动输出,因为这个ref是千层的ref
shallow.value.foo = 'change to another'
// 通过下面的强制触发副作用
triggerRef(shallow)

与vue2中的$forceUpdate()方法的异同:
相同点:都是用于手动强制更新组件或者响应式数据的方法!
不同点:

  1. triggerRef(): 主要用于手动触发一个响应式引用的更新,即使引用的值没有发生变化,特别对于浅层ref的依赖更新非常有用;
  2. $forceUpdate(): vue2中组件实例的一个方法,用于强制更新组件的视图,会强制触发组件的重新渲染,无论数据是否发生变化

关于该方法的定义如下:

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
export function triggerRef(ref: Ref){
triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value: void 0)
}
// 此枚举代表了响应式数据的不同脏状态级别,用于跟踪响应式数据的变化
export enum DirtyLevels {
NotDirty = 0, // 代表没有脏状态,即数据是干净的,没有发生变化
QueryingDirty = 1, // 代表正在查询脏状态,即正在进行脏状态的查询操作
MaybeDirty_ComputedSideEffect = 2,// 代表可能处于脏状态,可能由计算属性或者副作用引起的
MaybeDirty = 3, // 代表可能处于脏状态,但是具体原因未知
Dirty = 4 // 表示确实处于脏状态,即数据发生了变化
}
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any){
ref = toRaw(ref)
const dep = ref.dep
triggerEffects(
dep,
dirtyLevel,
__DEV__ ? {
target: ref,
type: TriggerOptypes.SET,
key: 'value',
newValue: newVal
}: void 0
)
}

🌟 上述关于triggerRef()方法主要是通过调用triggerRefValue()方法来实现强制视图的更新的,主要有三个参数:

  1. ref: 要触发更新的响应式引用;
  2. dirtyLevel: 更新的脏状态级别,即表示数据发生变化的程度;
  3. newValue: 新的值,用于更新响应式引用的值,如果不传入, 则表示不修改当前值,仅触发视图更新
customRef()

创建一个自定义的ref,显示声明对其依赖追踪和更新出发的控制方式,使用customRef可以创建一个具有自定义gettersetter行为的响应式引用,通常情况下,我们使用ref函数来创建响应式引用(创建的new RefImpl()实例),它将自动创建gettersetter,并将其绑定到内部的响应式数据上,但是有时候,我们如果想要更加灵活地控制gettersetter的行为,这时就可以使用customRef
customRef通过接收一个函数作为参数,这个函数接收一个track和一个trigger函数作为参数,并在函数内部返回一个对象,这个对象包含了自定义的gettersetter,关于该方法的定义如下:

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
export type CustomRefFactory<T> = {
track: () => void,
trigger: () => void
} => {
get: () => T,
set: (value: T) => void
}
class CustomRefImpl<T> {
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value() {
this._set(newVal)
}
}
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T>{
return new CustomRefImpl(factory) as any
}

🌟 customRef()方法的执行过程,就是创建一个CustomRefImpl实例的过程,上述CustomRefFactory类型是一个包含接收track以及trigger方法,然后返回带有get以及set方法的对象这里的tracktrigger方法无需我们去实现,因为在CustomRefImpl的构造函数中,它会自动地将这两个参数分别指向vue3中的响应式核心实现方法中,然后将对value的getset方法暴露出来,指向传入的自定义实现,借此来实现整个自定义响应式的目的!下面是对应的使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { customRef } from 'vue'
export function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}

这里通过自定义一composition API来创建一个自定义的响应式依赖实现,可以在value发生改变时延迟200毫秒更新视图,当然,这里也可以追加其他的逻辑的实现!!!

特殊attributes: ref

用于注册模版引用,一般接收一个字符串或者一个函数,用于注册元素或者子组件的引用,选项式API使用方式则存储在this.$refs中,组合式API时,引用则存储在与名字匹配的ref中,如下所示:

1
2
3
4
5
6
7
<script>
import { ref } from 'vue'
const p = ref()
</script>
<template>
<p ref="p">hellow</p>
</template>

如果将ref用于普通的DOM元素,引用将是元素本身,如果用于子组件的话,那么引用将是子组件的实例

TypeScript工具类型: MaybeRef、MaybeRefOrGetter

1
2
export type MaybeRef<T = any> = T | Ref<T>
export type MaybeRefOrGetter<T = any> = MaybeRef<T> | (() => T)

🌟 两种类型定义,MaybeRef代表是可能普通类型,也可能是ref类型,而MaybeRefOrGetter则代表还可能是getter函数类型