最近在学习 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