我一直觉得 ref 与 reactive 有各自的适用场景,单纯的使用 ref 一把梭与定义一个 reactive 把所有变量都塞进去都不是一个推荐的做法。
但是官网有明确说在组合式 API 中,推荐使用 ref() 函数来声明响应式状态
,这就让我对自己的理解产生了怀疑,难道自己这么久真的用错了吗?猛击查看官网-声明响应式状态
官网也说明了使用 reactive 定义响应式变量的一些局限性,算是解释了为什么推荐使用 ref。
日常使用
在日常开发中我一直是使用 reactive 定义对象,使用 ref 定义基本数据类型及数组。
使用 ref 定义数组是因为数组有时需要直接赋值,上边图里指出 reactive 是不能直接赋值的会失去响应性。如果按照这个理解使用 ref 定义对象也说的通。
那使用 reactive 定义的对象如何直接赋值还不会丢失响应呢?
按照官网的应该使用 ref 来定义可能会重新赋值的对象。但是我在实际使用中 Object.assign(oldObj, newObj);
一把梭。
第一种方式大概会有人这么用:
const state = reactive({
form: {
dateType: 30,
time: [],
},
unitList: [],
indicatorsList: [],
titleLineDom: null
})
把一个页面的所有变量都塞进一个 state
变量中当成 sfc 写法中 data
来用,使用 const { proxy } = getCurrentInstance()
获取下 proxy
来当 this,完美搭配!但我不是很喜欢,也不是很理解这种写法!
扯的有点远了,下边来看下 ref、 reactive 的具体实现。
reactive
先说 reactive 是因为 ref 的实现依赖了 reactive。
我们都知道 vue3 的响应式是通过 proxy
来实现的,那我们先来实现一个最简单的 reactive。
代码来自 Vue.js设计与实现
,当然这只是个简单的实现,实际实现要比这个复杂很多考虑很多边界情况。感兴趣的可查看 Vue.js设计与实现-对应的代码仓库查看,相比于 vue3 源码要更容易理解一些。
ref
在读 Vue.js设计与实现
时以为 ref 访问需要使用 .value
是因为 proxy
只能代理对象。基本数据类型需要封装为对象来实现数据的响应。代码来源
function ref(val) {
const wrapper = {
value: val,
};
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, "__v_isRef", {
value: true,
});
return reactive(wrapper);
}
在读源码的时候发现自己的理解好像有点不对,还有更多的深意,比如让你知道自己在使用的是一个响应式的变量。
源码位置:packages/reactivity/src/ref.ts :134
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
// 响应依赖集合
public dep?: Dep = undefined
// 用来判断是否是 ref
public readonly __v_isRef = true
// 类构造函数接收两个参数,初始值,是否是浅响应
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
// 设置原始值,toRaw 根据一个 Vue 创建的代理返回其原始对象。
this._rawValue = __v_isShallow ? value : toRaw(value)
// 浅响应设置初始值即可,否则使用 toReactive 设置为响应数据
// toReactive 判断值是否是对象,对象类型调用 reactive,非对象直接返回传入的值
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, newVal)
}
}
}
Vue.js设计与实现
的实现或许是为了让读者更容易理解与源码有些差异,但最终都实现了数据的响应。
当然 ref、reactive 的实现细节还有很多这个需要各位读者自行参悟,比如 reactive 对对象类型的响应是如何实现的 Vue.js设计与实现
书中给出了具体的实现细节建议阅读。
toReactive 函数
这里可以看出来 toReactive 的处理逻辑,是对象就进行包装都则直接返回传进来的值
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
有什么区别呢?
const refVal1 = ref(1)
// or
const refVal2 = reactive({ value: 1 })
我的理解
我依然坚持 ref、reactive 各有各的的适用场景,官网的话不要过度解读。reactive 存的问题 ref 就不会存在吗?
有限的值类型
我觉得也没什么问题设计如此,最初在学习 composition Api
使用的时候接收到的概念就是 ref 定义基本类型 reactive 定义对象。
使用 .value 来访问对象的属性我可能不太能接受。
const userInfo = ref({
name: '唐诗'
})
console.log(userInfo.value.name)
const userInfo = reactive({
name: '唐诗'
})
console.log(userInfo.name)
不能替换整个对象
那 ref 不能直接赋值啊!ref 直接对 value 属性直接赋值是不会丢失响应性,那使用 reactive 定义个一个 value,不也是一样的效果吗?
const refVal1 = ref(1)
// ref 直接赋值,一样会丢失响应
refVal1 = a
// or
const refVal2 = reactive({ value: 1 })
对解构操作不友好
这个其实我在日常使用中并没有什么感觉,使用 toRefs
包装一层也不算很麻烦。
ref 解构不也是一样吗?
总结
ref 与 reactive 有各自的适用场景,单纯的使用 ref 一把梭与定义一个 reactive 把所有变量都塞进去都不是一个推荐的做法。
至于官网的推荐,看个人理解了。之前的文章也说过 vue3 的 composition Api
是一把双刃剑,用的好与坏全凭个人本事。
当然你说官网推荐 ref 我也只能说你说的对,毕竟官网是一座大山!
文中我也有一些疑问,各位大佬若能提供解答不胜感激!