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

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

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...