Vue3 ref/toRefs 源码学习 | 8月更文挑战

2,018 阅读5分钟

前言

准备一个 demo

 ...
 <script src="../dist/vue.global.js"></script>
 <script>
 const { computed, ref, watch, watchEffect, createApp, reactive } = Vue
 debugger
 const refVal = ref(0)
 debugger
 </script>
 ...

ref

根据 debug 进入到 ref 的源码发现里边调用了 createRef,在当前这个文件中有两个地方调用了 createRef

  • ref 函数中 createRef(value)
  • shallowRef 函数中 createRef(value, true)

进入到 createRef 函数中,函数有两个参数

  • rawValue 是调用外层函数传入的原始值
  • shallowshallowRef 函数内部传入给 createRef 的值

函数内部也很简单,先是调用isRef 函数判断 rawValue 上的 __v_isRef 属性来判断 rawValue 是不是一个 ref 类型,如果是则 return rawValue,反之则将 RefImpl 的实例进行 return

下面看下 RefImpl 这个类

 class RefImpl<T> {
     private _value: T
 ​
     public readonly __v_isRef = true
 ​
     constructor(private _rawValue: T, public readonly _shallow: boolean) {
         this._value = _shallow ? _rawValue : convert(_rawValue)
     }
     get value() {
         // ...
     }
     set value(newVal) {
         // ...
     }
 }

先来看 constructor,会将 this._value 进行赋值,如果 _shallowtrue s说明是浅层响应对象,也就是通过 shallowRef 函数调用的,如果是这种情况则直接将 _rawValue 赋值;反之调用 convert 函数判断 _rawValue 是不是一个 object 类型的数据,是则调用 reactive 函数将其转换为响应式对象,反之将 _rawValue 进行 return,最终的结果都会赋值给 this._value

最终可以看到 demo 中定义的 refVal 的值如下

 {_rawValue: 0, _shallow: false, __v_isRef: true, _value: 0, value: 0}

这个时候根据我们的 demo,我们的调试就结束了。但是可以看到 RefImpl 类中还定义了 getset 函数。接下来需要更改一下我们的 demo 了。

get

RefImpl 类中的 get 函数如下

 get value() {
     track(toRaw(this), TrackOpTypes.GET, 'value')
     return this._value
 }

在 demo 中增加一个获取它的 value 属性的代码使其可以进入到 get 函数中

 console.log(refVal.value)

get 函数中做了两件事,一个是调用 track 函数收集依赖,通过 reactive 源码 这篇文章可以知道。再一个就是 return this._value

但是这里调用 track 函数就真的会收集依赖吗,因为 track 函数中一开始就有这么一个判断:

 if (!shouldTrack || activeEffect === undefined) {
     return
 }

这个问题在跟着断点走了一遍之后有了答案,答案是并不会,被 return 的原因是 activeEffect === undefined。也就是说目前只有调用 watchEffect 函数才会被收集依赖。

set

RefImpl 类中的 set 函数如下

 set value(newVal) {
     if (hasChanged(toRaw(newVal), this._rawValue)) {
         this._rawValue = newVal
         this._value = this._shallow ? newVal : convert(newVal)
         trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
     }
 }

get 函数一样,现有的 demo 仍然无法满足我们的需求,我们需要增加一个能够触发 set 函数的操作

 refObj.value = 321

在这里需要注意一下,如果你的 ref 声明的是一个 object 类型的数据,这里需要修改的是这个 object,而不是修改 object 里边的 value

 const refObj = ref({
  a: 1,
  b: 2
 })

需要做的是 refObj.value = 321 而不是 refObj.value.a = 3

进入 set 函数中,发现所有的操作都是在 newValue 的原始值和 oldValue 的原始值比对之后有变化才执行的。

  • toRaw 函数是获取传入值得原始值

  • hasChanged 函数比对两个参数是否相等。这个方法里有一段代码需要注意一下

     (value === value || oldValue === oldValue)
    

    这里是加入了 NaN === NaN 的判断,两个 NaN 是不相等的。

判断里边是将 newVal 给到 this._rawValue,这里的 _rawValue 用来存放未经过处理的原始值;下边的 _value 用来存放处理过的值,如果 this._shallowtrue_valuerawValue 是一样的,反之如果 newValue 如果是对象则会返回一个由 reactive 函数处理过的响应式对象,反之将 value 直接 return 这样看来只有是 object 类型的数据的时候 _rawValue_value 是不一样的,其他的都是一样的。

然后调用 trigger 函数来触发依赖。

以上就是所有的 ref 的源码。

总结

  • 整体看来 refreactive 的作用是一样的,都是返回了一个响应式的数据,只不过 ref 需要通过 .value 这个属性去访问。

toRefs

在使用 composition-api 的过程中我习惯这样使用

 ...
 const data = reactive({a: 1, b: 2})
 return {
     ...toRefs(data)
 }

这样在 template 中就可以直接使用 data 中定义的值了,否则还需要 data.xxx 来使用某一个值。接下来看一下 toRefs 的源码。

toRefs 的一开始就可以看到 toRefs 接收的参数是一个响应式对象,如果不是响应式对象则在开发环境下会有警告。

下面又判断了 object 是不是数组,是则创建一个和 object length 一样的新空数组,反之创建一个空对象,命名为 ret

然后遍历 object,调用 toRef 将每一个 key 对应的值转为 ref 并对应的赋值给 ret。代码如下

 const ret: any = isArray(object) ? new Array(object.length) : {}
 for (const key in object) {
     ret[key] = toRef(object, key)
 }

最终将 ret return

接下来看一下 toRef 函数做了什么。

toRef 函数通过 isRef 函数判断了 objectkey 对应的 value 是不是 ref,是则取对 value,反之通过 ObjectRefImpl 类将其封装为 ref,最终将其 return

这就是 toRefs 的所有代码。

延伸

: 为什么 reactive 包装的对象直接赋值会被覆盖丢失响应式,而 ref 包装的对象通过 value 赋值就不会丢失响应式呢?

: 因为其内部会对对应的 value 进行监听,所以当对 ref 对象 value 赋值时会触发 valueset 逻辑,从而进行响应性更新。

总结

通过 toRefs 可以将对象或者数组内的每一项都转换为 ref 类型,我们也就可以采用 .value 的形式访问。至于为什么在 template 中不需要采用 .value,是因为在模板编译的时候替我们省略了。

参考链接

Vue3.0(四)ref源码分析与toRefs