前言
原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null
等类型的值,在 JavaScript
中,原始值是按值传递的,引用类型是按引用传递的,这意味着,如果一个函数接收了一个原始值作为参数,那么形参和实参之间是没有引用关系的,它们是完全独立的两个值。在 JavaScript
中 proxy
无法对原始值提供代理,因此想要将原始值变成响应式数据,就必须要对原始值做一层包裹,这也就是 ref
要做的事。
为什么要引入 ref
用户自定义包裹对象的问题
根据上面提到的,如果要对原始值实现响应式数据,就必须要使用一个非原始值对象去 "包裹" 原始值,如:
// 原始值
let name = 'hello vue3'
// 包裹对象
const nameWrapper = {
value: 'hello vue3'
}
// 响应式数据
const name = reactive(nameWrapper)
// 触发响应式数据修改
name.value = 'hello vue3 reactive'
创建包裹对象的工作 看起来没有问题,但其实是有问题的:
- 用户为了创建一个响应式的原始值,不得不创建一个包裹对象
- 包裹对象的内容由用户自己定义,意味着不规范,即可以被随意命名,如
wrapper.value
和wrapper.val
都可以
引入 ref 解决问题
封装 ref 函数
为了解决上述的问题,可以 将创建包裹对象的工作 都封装到 ref
函数中,如:
// 封装 ref 函数
function ref(val){
// 创建包裹对象
const wrapper = {
value: val
}
// 创建响应式数据
return reactive(wrapper)
}
// 使用 ref
const name = ref('hello vue3')
effect(() => {
// 在副作用函数中通过 value 属性读取原始值
console.log(name.value)
})
// 修改 name.value 的值,触发 effect 副作用函数重新执行
nam.value = 'hello world'
优化 ref 函数
上面的代码已经可以实现最初的需求了,但是还是存在对应的缺陷,例如如何区分如下的两个响应式数据,是原始值的包裹对象,还是非原始值的响应式数据呢?
const name1 = ref('hello world')
const name2 = ref('hello vue3')
因此,需要对 ref
函数进行优化,如下:
function ref(val){
// 创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.definedProperty 在 wrapper 对象上定义一个不可枚举、不可写的属性 __v_isRef: true
Object.definedproperty(wrapper, '__v_isRef', {
value: true
})
// 创建响应式数据
return reactive(wrapper)
}
通过 Object.definedProperty
方法为包裹对象 wrapper
设置一个不可枚举、不可写的属性 __v_isRef
且值为 true
,用于去代表该对象是一个 ref
,而非原始值的响应式数据对象。
ref 用于解决响应式丢失问题
什么是响应式丢失?
在编写 Vue.js 组件时,通常都需要把数据暴露到模板当中使用,如:
export default {
setup(){
const state = reactive({foo: 1, bar: 2})
return { ...state }
}
}
可以看到这里使用了 展开运算符(...)的方式将响应式数据暴露到外部,然后这么做会丢失响应式,以下两段代码是等价的:
return { ...state }
等价于
return {foo: 1, bar: 2}
如果这么看,很容易发现 响应式丢失的原因 是向外暴露了一个普通对象,它不具有任何响应式的能力.
实现 toRef 函数处理响应式丢失
实际上就是通过返回一个类似 ref
结构的 wrapper
对象,但通过为 wrapper
对象实现 getter
函数,并在其中返回具体的响应式数据,即保证与原响应式数据之间的联系,实现如下:
function toRef(obj, key){
const wrapper = {
get value(){
return obj[key]
},
// 允许设置
set value(val){
obj[key] = val
}
};
// __v_isRef 定义,表明是一个 ref 类型
Object.definedProperty(wrapper, '__v_isRef', {
value: true
});
return wrapper;
}
// 使用
const state = reactive({foo: 1, bar: 2})
const newState = {
foo: toRef(state, 'foo'),
bar: toRef(state, 'bar')
}
封装 toRefs 函数
第一版本的 toRef
函数存在的不足,就是只能对的单个 key
进行处理,如果传入的响应式数据键非常多,那效果就不高了,于是通过封装 toRefs
函数实现批量转换:
function toRefs(obj){
const ret = {};
for(const key in obj){
// 组个调用 toRef 完成批量转换
ret[key] = toRef(obj, key)
}
return ret
}
为什么需要自动脱 ref
toRefs 带来的问题
toRefs
函数解决了响应式丢失的问题,但也带来了新的问题,那就是它会把响应式数据的第一层属性值转换为 ref
,意味着必须要通过 value
属性去访问值,这其实增加了用户的心智负担,因此,需要提供自动脱 ref
的能力。
自动脱 ref
所谓自动脱 ref
指的是属性的访问行为,即如果读取的属性是一个 ref
,则直接将该 ref
对应的 value
属性值返回。
可以基于前面定义的 __v_isRef
标识,通过使用 Proxy
为 toRefs
返回的对象创建代理对象,即通过代理来实现目标,上述内容封装成 proxyRefs
函数,如下:
function proxyRefs(target){
return new Proxy(target, {
get(target, key, receiver){
const value = Reflect.get(target, key, receiver)
// 自动脱 ref,如果是 ref 对象,则返回其 value 属性值
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver){
// 通过 target[key] 访问真实值
const value = target[key]
if(value.__v_isRef){
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
});
}
实际上,在编写 Vuej.s
组件时,组件中的 setup
函数所返回的数据会传递给 proxyRefs
函数进行处理,这也就是为什么在模板中直接访问 ref
时,不需要通过 value
属性访问和设置。
在 Vue.js
中,关于自动脱 ref
不仅存在上述场景外,reactive
函数也有自动脱 ref
的能力,例如:
const count = ref(0)
const obj = reactive(count)
// 直接访问
obj.count // 0
总结
ref
的作用场景总结如下:
- 处理 原始值 类型的响应式
- 解决 响应式 数据丢失
为了解决上述问题,引入了
ref
的概念,并且封装了toRef、 toRefs、proxyRefs
等函数,其中toRef、 toRefs
是解决具体问题,但同时产生了新的用户心智负担,如ref
的响应式数据,必须要通过.value
的方式访问。因此,又通过proxyRefs
函数实现 自动脱ref
,以减少用户心智负担。