vue3.4.5-从测试文件看ref源码

当看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,方便下一次新旧值的比较。

然后用toReactivenewValue变为响应式数据,然后更新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来判断当前的对象是不是一个通过reactivereadonly来创建的对象,isProxy的源码如下:

scss 复制代码
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

然后就是创建一个对象ret。然后遍历object对象上面的属性,把object上面所有的属性值都变成一个ref对象,只不过这个ref对象的value属性的get,set是没有依赖收集和依赖触发的内容的,这是因为第一步中已经保证object对象是个reactivereadonly对象,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,如果再遇到新的我会再次更新上面😄。

相关推荐
程序员大金42 分钟前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
道爷我悟了2 小时前
Vue入门-指令学习-v-html
vue.js·学习·html
无咎.lsy2 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
工业互联网专业3 小时前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
计算机学姐3 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
twins35204 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky4 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
杨荧5 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
Front思6 小时前
vue使用高德地图
javascript·vue.js·ecmascript
zqx_76 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架