当看vue
源码的时候,我们除了可以通过作者的提交注释中看懂代码的作用,我们可以通过看项目中的测试案例来反过来推导代码的作用。
ref
的源码在packages/reactivity/src/ref.ts,其对应的测试案例代码在packages/reactivity/tests/ref.spec.ts。
ref
测试代码:
typescript
it('should hold a value', () => {
// 普通值通过ref会返回一个新对象
const a = ref(1)
// 通过新对象的value属性能够访问到原始值
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
it('should be reactive', () => {
const a = ref(1)
let dummy
const fn = vi.fn(() => {
dummy = a.value
})
// effect 是执行副作用的函数,当把fn作为参数传递时,fn会执行一遍
// 执行fn代码: dummy = a.value
effect(fn)
// 此时的fn应该只执行一次
expect(fn).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
// 设置a.value,a是ref响应式数据,修改value属性会被触发依赖更新
// 此处就是effect中的fn会再次执行,dummy同时获得a.value的最新值
a.value = 2
expect(fn).toHaveBeenCalledTimes(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(fn).toHaveBeenCalledTimes(2)
})
上面是测试代码的部分解释,我们下面来看看对应的源码部分。
源码:
javascript
export function ref(value?: unknown) {
return createRef(value, false)
}
这边ref
内部调用了createRef
来创建,来看看createRef
的源码:
php
// 通过shallow来判断创建ref还是shallowRef
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
其中createRef
除了能创建ref
对象,还能创建shallowRef
对象。通过createRef
的第二个参数shallow
来区分。我们这边先看ref
对象。
createRef
首先会通过isRef
判断当前传入的值rawValue
是否已经是个ref的值,如果是则直接返回。对应了下面测试案例:
scss
it('should unwrap nested ref in types', () => {
const a = ref(0)
const b = ref(a)
expect(typeof (b.value + 1)).toBe('number')
})
isRef
通过对象内部属性__v_isRef
来判断,其对应源码如下:
arduino
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
createRef
最后一行是根据rawValue,shallow创建了一个RefImpl
对象,RefImpl
对象中就有很多额外属性,其中就包括__v_isRef
,来看看它的源码:
kotlin
class RefImpl<T> {
// ref创建的对象a中的value属性 一般是rawValue, 也有可能是reactive(rawValue)
private _value: T
// 原始值 也就是 createRef(rawValue, shallow) 中的rawValue
private _rawValue: T
public dep?: Dep = undefined
// 对象中有 __v_isRef 则此对象就是个ref对象
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
由上可见,RefImpl
定义的对象有5个属性。
我们来看看value属性,value属性有对应的set,get函数。其中get函数中trackRefValue(this)
收集了当前对该ref的依赖。在测试案例中就是effect
函数,也就是说我们通过访问value属性从而触发get函数中依赖收集过程,下次通过set函数的时候能够根据依赖的收集情况一次触发effect函数。
get函数的最后是返回了this._value
,这也是我们通过value属性能够访问到值的原因。
我们来看看this._value
,其中由于我们这边是ref
对象,所有的shallow都是false,此时_value的值是toReactive(value)
创建的值,toReactive
源码如下:
typescript
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
当value不是对象时直接返回,当value是对象的时候,会调用reactive
来深度响应式该数据。reactive
是vue3中专门给把对象变成响应式数据的方法,由于本次只看ref
的源码,reactive
不会做详细介绍。
也就是说ref
的传参是个对象,那这个对象中所有的属性,不管多深,都会就有响应式。可以看看下面这个测试案例:
scss
it('should make nested properties reactive', () => {
const a = ref({
count: 1,
})
let dummy
effect(() => {
dummy = a.value.count
})
expect(dummy).toBe(1)
a.value.count = 2
expect(dummy).toBe(2)
})
来看看value的set函数,由于我们shallow
是false,我们此时的set函数代码可以简化为:
kotlin
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
set函数中首先通过hasChanged
判断当前值是否发生了变化,hasChanged
的源码:
typescript
// compare whether a value has changed, accounting for NaN.
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
如果值变化了,我们把newValue
更新this._rawValue
,方便下一次新旧值的比较。
然后用toReactive
把newValue
变为响应式数据,然后更新this._value
。
最后调用triggerRefValue
触发依赖更新。
shallowRef
和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。
scss
test('shallowRef', () => {
const sref = shallowRef({ a: 1 })
expect(isReactive(sref.value)).toBe(false)
let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)
sref.value = { a: 2 }
expect(isReactive(sref.value)).toBe(false)
expect(dummy).toBe(2)
})
主要看上面的两块代码:
第一块:
scss
const sref = shallowRef({ a: 1 })
expect(isReactive(sref.value)).toBe(false)
第二块:
scss
sref.value = { a: 2 }
expect(isReactive(sref.value)).toBe(false)
第一块中shallowRef
创建的值没有响应式,第二块中给shallowRef
新赋的值同样不是响应式的:
我们看看源码中RefImpl
:
kotlin
class RefImpl<T> {
private _value: T
private _rawValue: T
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
在constructor中,当创建的shallowRef
时,我们默认返回了原有的值。所以测试代码中第一块中sref.value不是响应式的。
我们再看set函数中有个变量useDirectValue
,当原始值为shallowRef
创建的,新值是shallowRef创建的或者新值是只读的,此时useDirectValue
为true,当它为true时,hasChanged
有这样一段代码:
ini
this._value = useDirectValue ? newVal : toReactive(newVal)
this._value
的值为newVal
,而newVal
默认是没有响应式的。所以之前测试代码中给sref.value赋值默认也不是响应式的。
因此使用shallowRef
的时候要小心些,例如以下代码:
arduino
const refObj = ref({
name: 'ref'
})
const shallowRefObj = shallowRef({
name: 'shallowRef'
})
refObj.value.otherObj = shallowRefObj
shallowRef.value = {
name: 'shallowRef',
otherObj: refObj
}
console.log(refObj.value.otherObj.name) // => shallowRef
// refObj 不会被自动解包
console.log(shallowRef.value.otherObj.value.name) // => ref
toRefs
scss
test('toRefs', () => {
const a = reactive({
x: 1,
y: 2,
})
const { x, y } = toRefs(a)
expect(isRef(x)).toBe(true)
expect(isRef(y)).toBe(true)
expect(x.value).toBe(1)
expect(y.value).toBe(2)
// source -> proxy
a.x = 2
a.y = 3
expect(x.value).toBe(2)
expect(y.value).toBe(3)
// proxy -> source
x.value = 3
y.value = 4
expect(a.x).toBe(3)
expect(a.y).toBe(4)
// reactivity
let dummyX, dummyY
effect(() => {
dummyX = x.value
dummyY = y.value
})
expect(dummyX).toBe(x.value)
expect(dummyY).toBe(y.value)
// mutating source should trigger effect using the proxy refs
a.x = 4
a.y = 5
expect(dummyX).toBe(4)
expect(dummyY).toBe(5)
})
toRefs
接受一个响应式对象A,然后返回一个新对象B。这个新对象B中的属性和A中的属性名称一致,但是新对象B中访问的属性对应的值都是ref对象,通过value属性来访问他们的值。来看看toRefs
的源码:
typescript
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = propertyToRef(object, key)
}
return ret
}
首先通过isProxy
来判断当前的对象是不是一个通过reactive
,readonly
来创建的对象,isProxy
的源码如下:
scss
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
然后就是创建一个对象ret。然后遍历object对象上面的属性,把object上面所有的属性值都变成一个ref对象,只不过这个ref对象的value属性的get,set是没有依赖收集和依赖触发的内容的,这是因为第一步中已经保证object对象是个reactive
,readonly
对象,object的属性已经有get,set的依赖收集触发机制了,所以此处无需再次收集。
来看看propertyToRef
的源码:
scala
function propertyToRef(
source: Record<string, any>,
key: string,
defaultValue?: unknown,
) {
const val = source[key]
return isRef(val)
? val
: (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)
}
}
此处我们使用ObjectRefImpl
来创建,并没有使用RefImpl
来创建对象。原因就是object对象中属性已经有了get,set函数了,此处直接调用object属性的get,set函数即可,无需再次添加内容。
ref的测试文件中还有一些案例没有写进来,这些案例涉及到reactive
中的内容,我想把它们放到reactive
里面来看看,至于以上是我工作中用的多的几个ref,如果再遇到新的我会再次更新上面😄。