为什么需要ref
我们前两章讲讲解了reactive源码解析和effect源码解析,并且知道了它们是如何实现响应式的,还没看过的小伙伴可以先阅读一下。
我们回顾一下,reactive
函数可以创建通过Proxy
实现的响应式对象,响应式对象需要在effect
中使用才能收集到依赖,在更改响应式对象时,代理会通过trigger
通知所有依赖的effect
对象,并执行effect
的监听方法。
正因为reactive
创建的响应式对象是通过Proxy
来实现的,所以传入数据不能为基础类型,比如number
、string
、boolean
。
什么是ref
ref
对象是对reactive
不支持的数据的一个补充,让如基础数据响应式进行支持,以及更方便的对象替换操作推出的。下面我们先了解一下ref
的特性。
-
使用
ref
或shallowRef
函数创建ref
对象,ref
通过value
属性进行访问和修改传入参数。 -
与
reactive
不同,ref
的参数没有任何限制。 -
使用
reactive
可接受的对象为ref
参数对象时,isReactive(ref.value)
为true
。 -
ref
在effect
监听函数中使用可响应式 -
ref
在effect
中只有value
属性是可响应式的 -
customRef
可以创建自定义getter
、setter
的ref
,创建时需要提供一个创建get, set
工厂方法,工厂方法会传入收集方法和触发方法,由用户主动触发。如jslet value = 1 const custom = customRef((track, trigger) => ({ get() { track() return value }, set(newValue: number) { value = newValue trigger() } }))
-
使用
toRef
可以通过proxy
的某个属性生成为可以有默认值的ref
对象 -
使用
toRefs
可以通过proxy
的数据结构以及所有属性,生成与proxy
数据结构一致的,所有属性值为ref
对象的对象
综合上面的特性和之前讲解effect
的实现原理,能猜得到ref
对象会对value
属性的修改和获取时进行拦截,在value
被get
的时候收集依赖,在set
的时候获取依赖关联的effect
再触发依赖函数。ref
对属性修改和获取时不能通过proxy
来实现,ref
支持基础类型而proxy
不支持。收集依赖时不能使用effect
文件中的targetMap
关联effect
,targetMap
是WeakMap
类型,WeakMap
类型仅支持对象作为key
,不支持基础类型。
ref和shallowRef的具体实现
接下来我们看看ref
和shallowRef
的具体实现:
js
// 是否是ref根据属性的__v_isRef决定
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
// 创建ref对象,传入raw和是否是shallow
function createRef(rawValue: unknown, shallow: boolean) {
// 如果之前时ref则直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
与reactive
一样ref
创建的响应式对象也分为是否是shallow
,ref
对象支持对value
深度响应式,也就是说ref.value.a.b
中的修改都能被拦截,shallowRef
对象只支持对value
值的响应式。
ref
和shallowRef
函数都使用createRef
来创建ref
对象,只是参数的区别。创建的ref
对象会附加__v_isRef
属性来标识是否是ref
对象。在创建ref
对象之前会检查入参是否是ref
如果是就直接返回入参参数。
我们看到ref
函数创建的真实对象是RefImpl
,采用了class
写法,将raw
和shallow
作为构造函数,下面我们看看这个class
的实现:
js
// Ref对象类
class RefImpl<T> {
// 存放 reactive(raw) 后的proxy
private _value: T
// 存放 raw
private _rawValue: T
// 建立与effect的关系
public dep?: Dep = undefined
// 是否ref的标识
public readonly __v_isRef = true
// 构造,传入raw 和 shallow
constructor(value: T, public readonly _shallow: boolean) {
// 存储 raw
this._rawValue = _shallow ? value : toRaw(value)
// 如果是不是shallow则 存储 reactive proxy 否则存储传入参数
this._value = _shallow ? value : toReactive(value)
}
// getter value拦截器
get value() {
// track Ref 收集依赖
trackRefValue(this)
return this._value
}
// setter value拦截器
set value(newVal) {
// 如果是需要深度响应的则获取 入参的raw
newVal = this._shallow ? newVal : toRaw(newVal)
// 查看要设置值是否与当前值是否修改
if (hasChanged(newVal, this._rawValue)) {
// 存储新的 raw
this._rawValue = newVal
// 更新value 如果是深入创建的还需要转化为reactive代理
this._value = this._shallow ? newVal : toReactive(newVal)
// 触发value,更新关联的effect
triggerRefValue(this, newVal)
}
}
}
如果不是shallow
传入的value
会通过toReactive
转化为reactive
,然后存在ref._value
中。在get
的时候直接返回这个reactive
,这就是使用reactive
可接受的对象为ref
参数对象时,isReactive(ref.value)
为true
的原因,也是为什么能深度响应的原因。
ref
还会存储入参和set
的原始值,如果不是shallow
则通过toRaw
获取,存储在_rawValue
属性中,存储这个值是为了能正确的判断值是否被修改。所以下方这种情况是不会调用triggerRefValue
的,因为原始值是一样的。
js
const target = { name: 'bill' }
const reTarget = reactive(target)
const targetRef = ref(reTarget)
targetRef.value = target
ref
对象还有个非常重要的属性dep
,reactive
对象是通过targetMap
与Dep
关联的。reactive
收集时通过track
函数获取dep
,然后通过dep
对象调用trackEffects
函数来将effect
与Dep
关联。
reactive
触发时通过trigger
函数整理相关联的多个dep
最终合并成一个dep
,然后通过dep
调用triggerEffects
获取关联的effect
收集函数并触发。
dep
中的具体细节管理是通过trackEffects
函数和effect
对象管理的,将dep
与effect
是由trackEffects
函数处理的, 触发是由triggerEffects
函数执行的。
也就是说基于现有effect
的基础上,创建响应式对象只需要收集时获取dep
并调用trackEffects(dep)
, 触发时获取收集时的dep
并调用triggerEffects(dep)
。 dep
属性就是ref
能成为响应式对象的根本原因。
接下来我们看看ref
是如何实现trackEffects(dep)
和triggerEffects(dep)
的。ref
在get value
时会调用trackRefValue
,在set value
时,如果value
值发生了更改则调用triggerRefValue
。可以猜到这两个方法就是实现响应式的关键,接下来我们看看他们的具体实现
js
// 收集 ref 依赖 调用trackEffects(dep)
export function trackRefValue(ref: RefBase<any>) {
// 如果当前开启了跟踪
if (isTracking()) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref还未初始化dep则创建
if (!ref.dep) {
ref.dep = createDep()
}
// 如果是开发环境,则传入track细节,
if (__DEV__) {
trackEffects(ref.dep, {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep)
}
}
}
// 触发 ref 调用trackEffects(dep)
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref 有关联的dep
if (ref.dep) {
// 如果当前是开发环境则发送具体触发细节
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
// SET引起的变化
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
trackRefValue
会在适当的时候初始化dep
并调用trackEffects
,triggerRefValue
会获取ref
的dep
并调用triggerEffects
,就是我们上面说的内容。
大家注意到传入的ref
会调用toRaw
方法来重新赋值,这个方法是获取reactive
的原始数据的。因为用户可能使用reactive(ref(raw))
来获取数据,如果直接使用可能会收集到dep
属性的依赖。另外大家思考一下下面这段代码的effect
监听函数会触发几次?
js
const countRef = ref(0)
const reCount = reactive(countRef)
effect(() => {
console.log(reCount.value)
})
reCount.value = 3
答案是四次,第一次是首次收集依赖,reactive
会收到value
的获取,存储value
属性的dep
附加到targetMap
。然后调用ref.value
,ref
在获取value
时会调用trackRefValue
,创建dep
附加到自身属性上。注意ref.value
返回this._value
,这时候reactive
收到_value
属性的获取,存储_value
属性的dep
,附加到targetMap
中。所以创建了三个dep
。当发生更改新值存储到ref._value
中,而对于reactive
来说value
和_value
是完全没关联的所以会触发两次,而ref
自身会触发一次没所以一共是四次。
customRef
接下来我们看看自定义ref
方法customRef
是如何实现的:
js
// 自定义ref对象类
class CustomRefImpl<T> {
// 依赖dep 存储effets
public dep?: Dep = undefined
// 缓存getter setter
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
// mark ref
public readonly __v_isRef = true
// 传入ref工厂函数
constructor(factory: CustomRefFactory<T>) {
// 构建getter setter,传入track trigger函数
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
// 创建自定义ref
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
customRef
会创建CustomRefImpl
的一个实例并返回,CustomRefImpl
的实现和Ref
差不多,使用trackRefValue
和triggerRefValue
将dep
与effect
关联实现响应式。不过CustomRefImpl
会在工厂函数中传入trackRefValue
和triggerRefValue
,将收集依赖和触发执行权交给用户。让用户在适当的时候调用。在使用value
时候调用生产的get
方法,在设置value
是调用生产的set
方法。一般是在get
的时候调用收集函数,set
的时候触发函数。
toRefs和ObjectRefImpl
我们在使用reactive
时通过缓存属性值很可能会失去响应式特性。因为属性值可能是reactive
不支持深入响应的值,这时候缓存属性值,或者是通过ES6
解构出来的值是不具备响应特性的。比如在下面这两种使用方式:
js
const reuser = reactive({ name: 'bill', sex: '男' })
const { name } = reuser
const sex = reuser.sex
这样的话就得一直使用reuser.name
的方式来进行访问,vue
有两个api
能很好的解决这个问题,就是toRef
和toRefs
。
toRef
通过reactive
代理和代理的某个属性生成为ref
并且可以携带默认值。而toRefs
根据reactive
代理生成所有属性值为ref
的对象。生成的ref
的value
是代理属性值的映射,两端更改都会实时同步,我们看看是如何使用的:
js
const reuser = reactive({ name: 'bill', sex: '男' })
const name = toRef(reuser, 'name', '未命名')
const { sex } = toRefs(reuser)
console.log(reuser.name, reuser.sex) //bill 男
console.log(name.value, sex.value) //bill 男
name.value = 'lzb'
sex.value = '女'
console.log(reuser.name, reuser.sex) //lzb 女
console.log(name.value, sex.value) //lzb 女
delete reuser.name
console.log(reuser.name, reuser.sex) //undefined 女
console.log(name.value, sex.value) //未命名 女
接下来我们看看toRef
和toRefs
的具体实现:
js
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
// 记录代理
private readonly _object: T,
// 要辅助的key
private readonly _key: K,
// 默认值
private readonly _defaultValue?: T[K]
) {}
// 获取value
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
// 将proxy对象所有属性转化为ref值
export function toRefs<T extends object>(object: T): ToRefs<T> {
// 只有内部代理才能toRefs
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
// 分别对所有属性toRef
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
toRef
会先查看proxy[key]
是否是ref
如果是的话直接返回,如果不是则创建ObjectRefImpl
并且将参数传入,ObjectRefImpl
会标识当前对象是ref
类型 (通过__v_isRef
属性) ,并且缓存proxy
、key
和默认值。get value
时直接通过proxy[key]
来获取并返回,如果回去的值是undefined
则使用默认值。set value
时则通过proxy[key] = newVal
来设置。
toRefs
则是将每个属性都调用一次没有默认值的toRef
,并且返回与proxy
一致的数据结构。
为什么这里的ObjectRefImpl
类不需要dep
属性和收集依赖和触发更改呢?这是因为_object
属性本身是proxy
类型,当我们在使用proxy[key]
就实现了收集依赖,在proxy[key] = newVal
是就触发了更改。
其他辅助方法
ref
文件中还声明了其他辅助方法,比如triggerRef
手动触发ref
的更改使关联的effect
重新执行收集函数;unref
获取ref
的原始值。这两个方法比较简单直接看源码即可,这里就不再讲解了。
js
// 手动触发 ref
export function triggerRef(ref: Ref) {
// 开发环境用当前值做最新值变化
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
// 解构ref,直接返回value
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}
还有一个辅助方法proxyRefs
,这个方法将一个对象直属属性内的所有ref
属性值解构访问 (不需要通过value
下标访问) 。什么是直属属性就是第一层属性,比如下方的代码:
js
const name = ref('bill')
const unUser = proxyRefs({
name: name,
adderss: {
city: ref('珠海')
}
})
console.log(unUser.name) // bill
console.log(unUser.address.city) // Ref
unUser.name = 'lzb'
console.log(unUser.name) // lzb
proxyRefs
只对第一层属性的ref
解构。我们看看它的源码:
js
// 浅解构ref处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
// getter将unref方便访问
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
// setter先查看是否是ref,如果是则更新value
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 如果新数据不是ref但旧数据是则更新value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
// 创建代理ref 解构方便使用
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
// 如果是reactive对象则无需解构
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
当proxyRefs
入参是reactive
对象时则直接返回,reactive
对象本身会对ref
解构,而且是深度的,这里就不需要处理。有个特殊情况shallowReactive
对象不会对ref
解构,但是也直接返回了,也就是说这个方法对shallowReactive
对象时无效的!
如果proxyRefs
入参不是reactive
对象,则创建代理,get
拦截器通过unref
来获取值返回,set
拦截器通过判断当前要更新的是否是ref
如果是则更新value
。
到这里我们ref
的所有内容就已经讲完了,接下来日常小结。
小结
ref
对象自身附加了dep
,在收集依赖时通过trackEffects
函数,触发时通过triggerEffects
函数ref
能够创建深度响应式是依赖了reactive
Proxy
代理对象可以通过toRef
和toRefs
辅助方法保持对单个属性的引用,赋值修改会映射到Proxy
customRef
函数可以创建自由度极高的响应式对象