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了。

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

相关内容

热门资讯

安卓系统的如何测试软件,从入门... 你有没有想过,你的安卓手机里那些神奇的软件是怎么诞生的呢?它们可不是凭空出现的,而是经过一系列严格的...
小米8安卓系统版本,安卓系统版... 你有没有发现,手机更新换代的速度简直就像坐上了火箭呢?这不,小米8这款手机自从上市以来,就凭借着出色...
华为手机安卓系统7以上,创新体... 你有没有发现,最近华为手机越来越受欢迎了呢?尤其是那些搭载了安卓系统7.0及以上版本的机型,简直让人...
儿童英语免费安卓系统,儿童英语... 哇,亲爱的家长朋友们,你是否在为孩子的英语学习发愁呢?别担心,今天我要给你带来一个超级好消息——儿童...
ios系统切换安卓系统还原,还... 你有没有想过,有一天你的手机从iOS系统切换到了安卓系统,然后再从安卓系统回到iOS系统呢?这听起来...
灵焕3装安卓系统,引领智能新体... 你知道吗?最近手机圈里可是掀起了一股热潮,那就是灵焕3这款神器的安卓系统升级。没错,就是那个曾经以独...
安卓系统指南针软件,探索未知世... 手机里的指南针功能是不是让你在户外探险时倍感神奇?但你知道吗,安卓系统中的指南针软件可是大有学问呢!...
华为是不用安卓系统了吗,迈向自... 最近有个大新闻在科技圈里炸开了锅,那就是华为是不是不再使用安卓系统了?这可不是一个简单的问题,它涉及...
安卓系统热点开启失败,排查与解... 最近是不是你也遇到了安卓系统热点开启失败的小麻烦?别急,让我来给你详细说说这个让人头疼的问题,说不定...
小米max2系统安卓,安卓系统... 你有没有听说过小米Max2这款手机?它那超大的屏幕,简直就像是个移动的电脑屏幕,看视频、玩游戏,那叫...
电池健康怎么保持安卓系统,优化... 手机可是我们生活中不可或缺的好伙伴,而电池健康度就是它的生命力。你有没有发现,随着使用时间的增长,你...
安卓手机怎么调系统颜色,安卓手... 你有没有发现,你的安卓手机屏幕颜色突然变得不那么顺眼了?是不是也想给它换换“脸色”,让它看起来更有个...
安卓系统清粉哪个好,哪款清粉工... 手机用久了,是不是觉得卡得要命?别急,今天就来聊聊安卓系统清理垃圾哪个软件好。市面上清理工具那么多,...
华为被限制用安卓系统,挑战安卓... 你知道吗?最近科技圈可是炸开了锅!华为,这个我们耳熟能详的名字,竟然因为一些“小插曲”被限制了使用安...
安卓系统是不是外国,源自外国的... 你有没有想过,我们每天离不开的安卓系统,它是不是外国货呢?这个问题听起来可能有点奇怪,但确实很多人都...
安卓系统缺少文件下载,全面解析... 你有没有发现,用安卓手机的时候,有时候下载个文件真是让人头疼呢?别急,今天就来聊聊这个让人烦恼的小问...
kktv系统刷安卓系统怎么样,... 你有没有听说最近KKTV系统刷安卓系统的事情?这可是个热门话题呢!咱们一起来聊聊,看看这个新玩意儿到...
安卓系统连接电脑蓝牙,操作指南... 你有没有遇到过这种情况:手机里堆满了各种好用的应用,可就是想找个方便快捷的方式,把手机里的音乐、照片...
安卓车机11.0系统包,智能驾... 你有没有发现,最近你的安卓车机系统好像悄悄升级了呢?没错,就是那个安卓车机11.0系统包!这可不是一...
安卓系统最高到多少,从初代到最... 你有没有想过,你的安卓手机系统升级到哪一步了呢?是不是好奇安卓系统最高能到多少呢?别急,今天就来带你...