Vue响应式系统的作用与实现(三)
admin
2024-03-22 06:30:40
0

响应式系统的作用与实现(三)

前面讨论了非原始值的响应式实现,接下来这节将讨论原始值的响应式实现。原始值指的是:Boolean、String、Number、BigInt、Symbol、undefined和null等类型的值。

在JS中,原始值是按值传递的,而非按引用传递。这意味着如果一个函数接收原始值作为参数,那么形参和实参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。

另外JS中的Proxy无法提供对原始值的修改,因此想要将原始值变成响应式数据,就需要做一层包裹,vue.js里面是借助ref实现的,接下来就让我们来认识它。

1.引入ref的概念:

由于Proxy的代理目标必须是非原始值,所以我们没有任何手段拦截对原始值的操作,例如:

let str = 'hello world';
// 无法拦截对值的修改
str = 'hhh'

对于这个问题,我们能够想到的唯一办法:使用一个非原始值去包裹原始值,例如使用一个对象包裹原始值:

const wrapper = {value: 'hello world'
}
// 可以使用Proxy代理 warpper,间接实现对原始值的拦截
const name = reactive(wrapper);
// 修改值可以触发响应
name.value = 'hhh'

但是这样做有两个问题:

  • 用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对象;
  • 包裹对象由用户定义,这意味着不规范。用户命名将变得随意,如 wrapper.valuewrapper.val都是可以的。

为了解决这两个问题,我们封装一个ref函数,将包裹对象的创建工作都封装到该函数中:

// 封装一个ref函数
function ref(val) {// 在 ref函数内部创建包裹对象const wrapper = {value: val}// 将包裹对象变成响应式数据return reactive(wrapper);
}

这样便解决了上面的两个问题,但是还不够完善。

比如:如何区分refVal到底是原始值的包裹对象,还是一个非原始值的响应式数据?

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

从结果实现来看,它们没有任何区别,但是有必要区分下,一个数据到底是不是ref。

那么该如何区分一个数据是否是ref呢?

// 封装一个ref函数
function ref(val) {// 在 ref函数内部创建包裹对象const wrapper = {value: val}// 使用Object.defineProperty再wrapper对象上定义一个不可枚举的属性// __v_isRef,并且返回值为trueObject.defineProperty(wrapper, '__v_isRef', {value: true})// 将包裹对象变成响应式数据return reactive(wrapper);
}

通过检查 __v_isRef属性便知道一个数据是否是ref了。

2.响应丢失问题:

ref除了能够用于原始值的响应式之外,还能用来解决响应丢失问题。

什么是响应丢失问题:

const obj = reactive({ foo: 1, bar2 })
// 将响应式数据展开到一个新的对象newObj里
const newObj = {...obj
}
effect(() => {// 在副作用函数内通过新的对象 newObj读取foo的值console.log(newObj.foo);
})
// 此时修改obj.foo不会触发响应
obj.foo = 100;

使用拓展运算符展开的新对象newObj是一个普通对象,不具备任何响应能力,所以当我们修改 obj.foo的值时,不会触发副作用函数重新执行。

那么有什么办法可以让我们实现:在副作用函数内,即使通过普通对象 newObj来访问属性值,也能够建立响应联系?

const obj = reactive({ foo: 1, bar2 })
// 将响应式数据展开到一个新的对象newObj里
const newObj = {foo: {get value() {return obj.foo}},bar: {get value() {return obj.bar}}
}
effect(() => {// 在副作用函数内通过新的对象 newObj读取foo的值console.log(newObj.foo.value);
})
// 这样就能够触发响应了
obj.foo = 100;
  • newObj对象通过访问器读取value的值时,读取到的其实是obj下的同名属性值。即当在副作用函数内读取 newObj.foo时,等价于间接读取了 obj.foo的值。这样响应式数据自然能够与副作用函数建立响应联系。当修改值时,也能触发副作用函数重新执行了。
  • foo和bar的结构类似,这时我们可以将它抽象出来进行封装。
function toRef(obj, key) {const wrapper = {get value() {return obj[key];}}// 定义 __v_isRef 属性,表面这是一个refObject.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper;
}
// 借助toRef重新实现newObj对象
const newObj = {foo: toRef(obj, 'foo'),bar: toRef(obj, 'bar')
}

进一步实现,如果obj的键非常多,我们可以封装toRefs函数,来批量完成转换:

function toRefs(obj, key) {const ret = {};// 使用 for...in 循环遍历对象for (const key in obj) {// 逐个调用toRef完成转换ret[key] = toRef(obj, key);}return ret;
}

这样一步操作便可完成对一个对象多个键的转换了:

const newObj = { ...toRefs(obj) }

如此一来,响应丢失问题就彻底解决了。思路是:将响应式数据转换成类似于ref结构的数据,并且为了概念上的统一,将通过toRef或toRefs转换后得到的结果视为真正的ref数据。

上文实现的toRef函数还存在缺陷,即通过toRef函数创建的ref是只读的,因为我们只添加了getter,没有添加setter,最终实现如下:

function toRef(obj, key) {const wrapper = {get value() {return obj[key];},// 允许设置值set value(newValue) {obj[key] = newValue;}}// 定义 __v_isRef 属性,表面这是一个refObject.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper;
}

3.自动脱Ref:

toRefs函数解决了响应丢失的问题,但是toRefs会把响应式数据的第一层属性值转换为ref,因此必须通过 value属性访问之,如:

const obj = reactive({ foo: 1, bar2 });
// obj.foo , obj.bar 即可访问属性值
const newObj = { ...toRefs(obj) };
newObj.foo.value;
newObj.bar.value;

这明显写起来很繁琐,增加了心智负担,毕竟通常是在模板里面访问数据,肯定不希望写得太烦吧, {{foo.value}} 、 {{bar.value}}

因此我们需要自动脱ref的能力。所谓自动脱ref,指的是属性的访问行为,即如果读取的属性是一个ref,则直接将该ref对应的value属性值返回即可:

newObj.foo.value  ->  newObj.foo

要实现这个功能,需要使用Proxy为newObj创建一个代理对象,通过代理来实现最终目标,这时就需要用到上文中的ref标识:

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;}})
}
// 调用proxyRefs 函数创建代理
const newObj = proxyRefs({ ...toRefs(obj) })

这样,当我们读取的属性是一个ref时,直接返回该ref的value属性值,就实现了自动脱ref了。

console.log(newObj.foo);
console.log(newObj.bar);

在Vue3中,我们用到的setup函数所返回的数据其实是给 proxyRefs处理的。(script setup真的好香

const myComponent = {setup() {const count = ref(0);// 返回的这个对象会传递给 proxyRefsreturn { count };}
}

这便是在访问直接访问ref值时,无需通过value属性来访问。上面我们只做了读取的,还没做设置的,添加对应的set拦截函数即可:

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读取真实值const value = target[key];// 如果值是Ref,则设置其对应的value属性值if (value.__v_isRef) {value.value = newValue;return true;}return Reflect.set(target, key, newValue, receiver);}})
}

在Vue.js中,其实reactive也有自动脱ref的能力,比如:

const count = ref(0);
const obj = reactive({ count });
obj.count; //0

这样我们不用知道一个值到底是不是ref,在模板做使用响应式数据时,无需关心它是不是ref。

4.总结:

在本章中,介绍了ref的概念,它本质就是一个 “包裹对象”,因为JS的Proxy无法提供对原始值的代理,所以需要使用一层对象作为包裹,间接实现原始值的响应式方案,并且定义一个属性 __v_isRef作为ref的标识。以及解决响应丢失,自动脱ref的能力,减轻了心智负担,暴露到模板中的响应式数据不用写 xxx.value了。

至此,响应式系统的作用与实现便介绍到这里。

相关内容

热门资讯

怎么解除订阅安卓系统,安卓系统... 你是不是也和我一样,手机里订阅了好多服务,结果现在想解除订阅,却一头雾水?别急,今天就来手把手教你如...
安卓系统停用怎么开启,轻松恢复... 亲爱的手机控们,你是否曾经遇到过安卓系统突然停用的情况,让你手忙脚乱,不知所措?别担心,今天就来教你...
安卓系统电池健康度,电池健康度... 你有没有发现,你的安卓手机最近是不是有点儿不给力了?电池续航能力大不如前,充电速度也慢了不少?别急,...
安卓系统按键怎么截图,安卓系统... 你是不是也和我一样,有时候想截个图分享给朋友,却发现安卓手机的截图功能有点神秘呢?别急,今天就来手把...
购票系统安卓源代码,架构设计与... 你有没有想过,那些我们每天离不开的购票系统,它们背后的秘密是什么呢?今天,就让我带你一探究竟,揭开购...
安卓手机系统后台测试,深度解析... 你有没有发现,你的安卓手机后台总是悄悄地忙碌着?别小看了这些后台程序,它们可是手机系统稳定运行的关键...
安卓系统重启的图标,解锁设备新... 手机突然重启,是不是心里有点慌?别急,今天就来和你聊聊安卓系统重启的图标,让你一眼就能认出它,再也不...
车载智慧屏安卓系统,智能出行新... 你有没有发现,现在的车载智慧屏越来越智能了?尤其是那些搭载了安卓系统的,简直就像是个移动的小电脑,不...
安卓系统连上网权限,解锁设备无... 你有没有发现,你的安卓手机里有些应用总是偷偷连上网?别小看这个小小的网络权限,它可是能影响你隐私、消...
安卓谷歌操作系统,探索安卓谷歌... 你知道吗?在智能手机的世界里,有一个操作系统可是无人不知、无人不晓,那就是安卓谷歌操作系统。它就像一...
安卓系统手写%怎样调出,具体实... 你有没有遇到过这种情况:在使用安卓手机的时候,突然想用手写输入法来记录一些灵感或者重要信息,可是怎么...
安卓手机重置 系统设置,轻松恢... 手机用久了是不是感觉卡顿得厉害?别急,今天就来教你怎么给安卓手机来个大变身——重置系统设置!想象你的...
win如何安装安卓系统,Win... 哇,你有没有想过,让你的Win系统也能玩转安卓应用?没错,就是那种在手机上轻松自如的安卓系统,现在也...
苹果qq和安卓系统,跨平台体验... 你有没有发现,现在手机市场上,苹果和安卓的较量可是越来越激烈了呢!咱们就来聊聊这个话题,看看苹果QQ...
显示最好的安卓系统,探索最新旗... 你有没有想过,为什么安卓系统那么受欢迎呢?它就像一个魔法盒子,里面装满了各种神奇的魔法。今天,就让我...
安卓app怎么降级系统,系统版... 你有没有发现,有时候安卓手机的系统更新后,新功能虽然炫酷,但老系统用起来更顺手呢?别急,今天就来教你...
雷军脱离安卓系统,引领科技变革... 你知道吗?最近科技圈可是炸开了锅,因为我们的雷军大大竟然宣布要脱离安卓系统,这可真是让人大跌眼镜啊!...
安卓系统自动开网络,安卓系统自... 你有没有发现,手机里的安卓系统有时候会自动开启网络连接,这可真是让人又爱又恨啊!有时候,你正专心致志...
安卓系统怎样控制后台,因为服务... 手机里的安卓系统是不是感觉越来越卡了?后台程序太多,不仅耗电还影响性能。别急,今天就来教你怎么巧妙地...
安卓系统打游戏推荐,一触即达! 你有没有发现,现在手机游戏越来越好玩了?不管是休闲小游戏还是大型MMORPG,都能在手机上畅玩。但是...