Vue3 的 toRef 与 toRefs

487 阅读3分钟

最近在学习 Vue3 的 ref 实现原理,Vue3 是怎么对原始值实现响应性的呢?,我们知道 Vue3 的 Proxy 方案的代理目标是非原始值,而对 原始值,Vue3 是怎么去实现的呢,其实也是包裹了一层reactive。 // JS 中 原始值是按值传递,非原始值是按引用传递;

我们一般说 reactive 用于包裹 数组对象,而ref用于包裹 原始值,其实 ref 也可以用于包裹 对象数组

// 封装 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
	const wrapper = {
		value: val
	}
	return reactive(wrapper)
}

我们对比一下两行代码,从实现方案来说,是没有区别的;

cosnt refVal1 = ref(1)
const refVal2 = reactive({value: 1})

但是一个是 ref,一个是 reactive,如何进行区分呢?需要再原型属性添加 __v_isRef

function ref(val) {
	const wrapper = {
		value: val
	}
	// 通过 Object.defineProperty 在 wrapper 对象上添加一个不可枚举的属性 __v_isRef,并将该值设定为 true,用于区分 ref
	Object.definProperty(wrapper,'__v_isRef',{
			value: true
	})
	return reactive(wrapper)
}

toRef 与 toRefs

ref 除了用来解决原始值的响应性问题,还有用于解决响应性丢失问题

// js
export default{
 setup(){
 	const obj = reactive({
 		foo: 1, bar: 2
 	})
 	
 	// !!! 1s后对 obj 的数据进行修改,并不会触发更新
 	setTimeout(()=>{
 		obj.foo = 3
 	},1000)
 	return {
 		...obj
 	}
 }
}
// template
<template>
<p>{{foo}} -- {{bar}}</P>
</template>

以上这种写法导致 obj 失去了响应性,后续对这个 obj 的值进行修改并不会触发;

而这种破坏响应性的语法就在与 es6 的展开运算符中(...)

return{
...obj
}
// 相当于,返回一个定值,肯定就解耦了响应性;
return {
	foo: 1,
	bar: 2,
}

为了响应丢失问题,我们可以包裹 toRef 函数,保留这种对象的按引用传递的链路

function toRef(obj,key){
	const wrapper = {
	// 通过访问 obj 对象中的访问器属性 value,当读取 value 值,其实是读取 obj 下的属性值,这样就可以保留对象的按引用传递,保留响应性;
		get value(){
			return obj[key]
		}
	}
	return wrapper
}

setup 的 return 上的可以这样子写

return {
	foo: toRef(obj,'foo')
	bar: toRef(obj,'bar')
}

但是这种写法有点繁琐,我们就可以再包裹一个 toRefs,进行批量转换

function toRefs = function(obj){
	const ret = {}
	for(const key in obj){
		ret[key] = toRef(obj,key)
	}
	return ret
}

之后的调用就可以变成了以下展示方式,最简化的语法保留了响应性;

	return {
		...toRefs(obj)
	}

附带一个完整版的 toRef 方法,添加 __v_isRef 不可枚举属性,同时可以对 toRef 的属性值进行 setter 值;

function  toRef(obj,key){
	const wrapper = {
		get value(){
			return obj[key]
		}
		// 可设定值,原来的 toRef 的值是只读;
		set value(valu){
		 obj[key] = val
		}
	}
	// 添加 isRef 的标识值;
Object.definProperty(wrapper,'__v_isRef',{
			value: true
	})
	return wrapper
}

自动脱 Ref

toResf 解决了 ref 的响应依赖问题,但是带来了一个语法问题,它为 reactive 的第一层数据都增加了 ref,当需要访问 ref 的值,我们都需要通过 .value ,某种程度增加了心智负担(经常容易忘了写,容易报错)

const obj = reactive([
	foo: 1, 
	bar: 2
])
const newObj =  {...toRefs(obj)}
newObj.foo.value = 10; // 赋值,读取,都显得十分麻烦;

但是如果万一 toResf 的第一层不是代理的 ref 呢(可能又是一个对象),那么访问 value 的 时候,就不需要 .value 进行读取,直接读取该对象就可以获取值;

// 定义的 __v_isRef 就派上用场了
function isRef(val){
	return val.__v_isRef ? val.value : val
}

我们就可以写一个完成的自动脱 Ref 的方法

function proxyRefs(target){
	return new Proxy(target, {
	get(target,key,receiver){
		const value = Reflect.get(target,key,receiver)
		return value.__v_isRef ? value.value : value
	}
	set(target, key, newVal, receiver){
		// 通过 target 获取真实值
		const value = target[key]
		// 如果是 ref,则通过 ref 的值进行设定
		if(value.__v_isRef){
			value.value = newVal
			return true
		}
		return Reflect.get(target,key,newVal,receiver)
	}
	})
}

在后续的调用中,就相对方便很多

const foo = ref(1)
const obj = reactive({foo})
obj.foo // 1