前言
准备一个 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是调用外层函数传入的原始值shallow是shallowRef函数内部传入给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 进行赋值,如果 _shallow 为 true 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 类中还定义了 get 和 set 函数。接下来需要更改一下我们的 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 里边的 valueconst 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._shallow为 true 则 _value 和 rawValue 是一样的,反之如果 newValue 如果是对象则会返回一个由 reactive 函数处理过的响应式对象,反之将 value 直接 return 这样看来只有是 object 类型的数据的时候 _rawValue 和 _value 是不一样的,其他的都是一样的。
然后调用 trigger 函数来触发依赖。
以上就是所有的 ref 的源码。
总结
- 整体看来
ref和reactive的作用是一样的,都是返回了一个响应式的数据,只不过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 函数判断了 object 中 key 对应的 value 是不是 ref,是则取对 value,反之通过 ObjectRefImpl 类将其封装为 ref,最终将其 return。
这就是 toRefs 的所有代码。
延伸
问: 为什么 reactive 包装的对象直接赋值会被覆盖丢失响应式,而 ref 包装的对象通过 value 赋值就不会丢失响应式呢?
答: 因为其内部会对对应的 value 进行监听,所以当对 ref 对象 value 赋值时会触发 value 的 set 逻辑,从而进行响应性更新。
总结
通过 toRefs 可以将对象或者数组内的每一项都转换为 ref 类型,我们也就可以采用 .value 的形式访问。至于为什么在 template 中不需要采用 .value,是因为在模板编译的时候替我们省略了。