理解 Proxy 和 Reflect

news2024/12/23 2:14:54

03_02_理解 Proxy 和 Reflect

一、开始之前:

为什么还会有这一篇文章呢?不是手写mini-vue吗?其实可以理解成支线任务、番外篇,是对主线内容的补充。

这一篇文章可能文字比较多,理论知识比较多,参考了4本书相关的章节写的。可以泡杯咖啡或者喝杯茶,坐下来慢慢看哦。☕️

二、为什么使用Proxy?

众所周知,vue3的响应式是靠Proxy代理对象实现的。

代理是使用Proxy构造函数创建的。这个构造函数接收两个参数:目标对象target和处理程序对象handler。缺少其中任何一个参数都会抛出TypeError

使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。例如:get和set都知道就不说了,apply可以用来捕获函数的调用操作。

每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。大致可以理解为代理对象目标对象前设置了一层“拦截”层。

那这样,对于对象属性的读取和设置,我们就可以感知到,只有在这个基础之上,我们才能去实现响应式。

既然我们知道了为什么用Proxy,那接下来就来看看Proxy到底是什么?

三、Proxy是什么?

《JavaScript高级程序设计(第4版)》

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

从很多方面看,代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。 但直接操作会绕过代理施予的行为。

《ES6标准入门》

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于 种“元编程”( meta programming ),即对编程语言进行编程。

Proxy 可以理解成在目标对象前架设 个“拦截”层 ,外界对该对象的访问都必须先通过 这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写。

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

《深入理解ES6》

通过调用 new Proxy() ,你可以创建一个代理用来替代另一个对象(被称为目标),这个代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。

代理允许你拦截在目标对象上的底层操作,而这原本是 JS 引擎的内部能力。拦截行为使用了一个能够响应特定操作的函数(被称为陷阱)。

(一)概述

从以上书籍中的描述,我们可以大概总结一下:使用 Proxy 可以创建一个代理对象,它能够实现对 其他对象 的代理。

这里的关键词有两个:

1.“创建” : 意为代理对象这是一个新对象。
2.“其他对象” : 只能代理对象,无法代理非对象值,例如:数字、字符串、布尔类型。

那么,代理指的是什么呢?

所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。这句话的关键词比较多,我们逐一解释。

(二)基本操作

前文也提到了基本操作,这里又说到了基本语义,那么什么样的才是基本的呢?

const obj = { foo: 1 };

obj.foo; // 读取属性 foo 的值
obj.foo++; // 读取和设置属性 foo 的值 

给出一个对象,我们可以读取某个属性的值,同样也可以设置某个属性的值。

类似这种读取、设置属性值的操作,就属于基本语义的操作,即基本操作。当然,勿6!可以理解成单步最简动作,而不是复合动作

既然是基本操作,那么它就可以使用Proxy拦截:

const p = new Proxy(obj, {// 拦截读取属性操作get() { /*...*/ },// 拦截设置属性操作set() { /*...*/ }
}) 

JavaScript中,万物皆对象

那么函数自然也不例外,例如一个函数也是一个对象,所以调用函数也是对一个对象的基本操作

const fn = (name) => {console.log('我是:', name)
}

// 调用函数是对对象的基本操作
fn() 

因此,我们可以用 Proxy 来拦截函数的调用操作,这里我们使用 apply 拦截函数的调用:

const p2 = new Proxy(fn, {// 使用 apply 拦截函数调用apply(target, thisArg, ...argumentsList) {return Reflect.apply(...arguments);}
})

p2('IamZJT') // 输出:'我是:IamZJT' 

(三)复合操作

既然有基本操作,那对应的就有复合操作

调用一个对象下的方法就是典型的复合操作

objj.fn(); 

实际上,调用一个对象下的方法,是由两个基本操作组成的。

第一个基本操作是 get,即先通过 get 操作得到 obj.fn 属性。第二个基本操作是 函数调用,即通过 get 得到 obj.fn 的值后再调用它,也就是我们上面说到的 Reflect.apply

四、Reflect又是什么?

Reflect又叫反射,设计的目的主要有以下几个:

(1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2)修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

// 老写法
try {Object.defineProperty(target, property, attributes);// success
} catch (e) {// failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {// success
} else {// failure
} 

(3)让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为,。

(4)其实可能你已经注意到了,Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。Proxy可以捕获13种不同的基本操作,这些操作有各自不同的Reflect API方法。

这里稍微列举一下:

  • Reflect.get() → 读取属性
  • Reflect.set() → 设置属性
  • Reflect.has() → 属性是否存在,等同于in
  • Reflect.defineProperty() → 定义属性
  • Reflect.getOwnPropertyDescriptor() → 获取指定属性的描述对象
  • Reflect.deleteProperty() → 删除属性,等同于delete
  • Reflect.ownKeys()() → 返回自身属性的枚举
  • Reflect.getPrototypeOf() → 用于读取对象的__proto__属性
  • Reflect.setPrototypeOf() → 设置目标对象的原型(prototype)
  • Reflect.isExtensible() → 表示当前对象是否可扩展
  • Reflect.preventExtensions() → 将一个对象变为不可扩展
  • Reflect.apply() → 调用函数,等同于等同于 Function.prototype.apply.call(),但借用原型方法可读性太差
  • Reflect.construct() → 等同于new

到现在,可能有人要说了,你说了这么一大堆七七八八的,看也没怎么看明白。上篇文章的坑不还是没填,到现在还是不清楚为什么要用Reflect.getReflect.set

五、vue3中为什么使用Reflect?

不要着急,有了上篇文章的响应式基础和这些前置知识,我们就能知道为什么要使用Reflect了。

其实一句话就能总结:Reflect.get还有第三个参数,即指定接收者receiver,你可以把它理解为函数调用过程中的 this

const obj = { foo: 1 };

const result = Reflect.get(obj, 'foo', { foo: 2 });

console.log(result); // 输出的是 2 而不是 1 

我们看一下reactive的实现里面不用Reflect的情况:

const obj = { foo: 1 }

const p = new Proxy(obj, { get(target, key) {track(target, key);return target[key];},set(target, key, newVal) {target[key] = newVal;trigger(target, key);}
}) 

乍一看,似乎没什么问题。确实,大多数情况下,两者没什么区别。那么到底什么时候会出问题呢?接着往下看。

首先,我们修改一下obj对象,为它添加bar属性:

const obj = {foo: 1,get bar() {return this.foo;}
} 

可以看到:bar属性是一个访问器属性,它返回了this.foo属性的值。接着,我们在effect中通过代理对象p访问bar属性。

effect(() => {console.log(p.bar); // 1
}) 

首先effect首次执行收集依赖的时候,会读取p.bar属性,它发现p.bar是一个访问器属性,因此执行getter函数。由于在getter函数中通过this.foo读取了foo属性值,因此我们认为effect中的依赖会被作为与foo属性的依赖收集起来。

转而当我们修改p.foo的值时,依赖应该被重新触发,p.bar应该是2才对。然而实际并非如此,当我们尝试修改p.foo的值时:

p.foo = 2; 

依赖并没有重新执行,奇了怪了?实际上,问题就出在bar属性的访问器函数getter里,也就是代理中的this指向问题

const obj = {foo: 1,get bar() {// 这里的this指向哪里return this.foo;}
} 

当我们使用obj读取bar属性值时,这里的this指向哪里呢?那当我们用代理对象p访问bar,这时候this又指向的哪里呢?

很显然,方法中的this通常指向调用这个方法的对象。

那接着,我们来回顾一下整个流程。首先,我们通过代理对象p访问p.bar,这会触发代理对象的get拦截函数执行:

const p = new Proxy(obj, {get(target, key) {track(target, key)// 注意,这里我们没有使用 Reflect.get 完成读取return target[key]; }, // 省略部分代码
}) 

get拦截函数内,通过target[key]返回属性值。其中target是原始对象obj,而key就是字符串'bar',所以target[key]相当于obj.bar。因此,当我们使用p.bar访问bar属性时,它的getter函数内的this指向的其实是原始对象obj,这说明我们最终访问的其实是obj.foo

很显然,这里没有响应式对象,所以自然依赖也不会进行收集。因为在副作用函数内通过原始对象访问它的某个属性,这等价于:

effect(() => {// obj 是原始数据,不是代理过的对象,这样的访问不能够建立响应联系// 这里也就是上文中开头引用中提到的:直接操作会绕过代理施予的行为。obj.foo;
}) 

因为这样做不会收集依赖,所以无法触发响应的问题也就明了了。

那么这个问题应该如何解决呢?这时Reflect.get的第三个参数receiver就派上用场了。

const p = new Proxy(obj, {// 拦截读取操作,接收第三个参数 receiverget(target, key, receiver) {track(target, key)// 使用 Reflect.get 返回读取到的属性值return Reflect.get(target, key, receiver)},// 省略部分代码
}) 

如上面的代码所示,代理对象的get拦截函数接收第三个参数receiver,它代表谁在读取属性,例如:

p.bar // 代理对象 p 在读取 bar 属性 

当我们使用代理对象p访问bar属性时,那么receiver就是p,你可以把它简单地理解为函数调用中的this。 那么此时,访问器属性bargetter函数内的this的指向就是代理对象p

const obj = {foo: 1,get bar() {// 现在这里的 this 为代理对象 preturn this.foo;}
} 

this由原始对象obj变成了代理对象p。那么,依赖此时就能正常进行收集。如果此时再对p.foo进行set操作,会发现已经能够触发依赖重新执行了。

正是基于上述原因,vue3的响应式系统采用了Reflect.*方法,而我们的mini-vue也同样如此。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/113952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

九、Express 基本使用(简)

前一篇内容讲到Express框架的安装以及对Express项目的目录文件有一定的认识了解之后,使用Express创建了最基本的一个Web服务器,接下来进行对Express框架的一些内容来做一个基本的使用; 创建 Web 服务器 node 或 nodemon 执行app.js文件&#…

踩坑了、踩到一个特别无语的常识坑

大家好 踩坑了啊,又踩坑了啊! 这次踩到一个特别无语的常识坑。知道真相的那一刻,人就是整个麻掉。 先上个代码: private static double calculate(double a, int b) {return a / b; } 复制代码 你先别问为什么计算不用 BigDec…

RxJS初认识

概念: RxJS的运行就是Observable和Observer之间的互动游戏。 Observable就是“可以被观察的对象”,即“可被观察者”,而Observer就是‘观察者’,连接两者的桥梁就是Observable对象的函数subscribe。 RxJS中的数据流就是Observable…

第二十三章 数论——质数(1)(超级详细的推导)

第二十三章 数论——质数一、什么是质数二、质数的判断1、试除法(朴素版)2、试除法(优化版)三、分解质因数1、什么是质因数2、算术基本定理3、分解质因数(1)问题(2)思路(…

RepNAS: 基于NAS的结构重参数化技术

1. 介绍 在过去几年里,NAS技术取得了长足进展。然而,由于搜索约束与实际推理之间的差异导致高效网络搜索仍极具挑战性。为搜索一个具有高性能、低推理延迟的模型,已有方案往往在算法中添加计算复杂度约束。然而,推理速度会受多种…

【强化学习笔记】马尔可夫过程、马尔可夫奖励过程

文章目录1.马尔可夫过程1.1.随机过程1.2.马尔可夫性质1.3.马尔可夫过程2. 马尔可夫奖励过程2.1.回报2.2.价值函数3.马尔可夫决策过程1.马尔可夫过程 马尔可夫过程(Markov process) 指具有 马尔可夫性质 的 随机过程 ,也被称为马尔可夫链&…

C++GUI之wxWidgets(4)-编写应用涉及的类和方法(2)-wxDialog,wxCloseEvent

目录wxDialog包含类继承具体描述模态和无模态支持样式此类发出的事件wxWindow:&#xff1a;Close()wxCloseEvent具体描述使用此类的事件wxDialog 包含 #include <wx/dialog.h>类继承 描述主 具体描述 对话框是一个带有标题栏的窗口&#xff0c;有时还有一个系统菜单…

python-多线程、网络编程、正则表达式

目录 闭包 多线程 主线程 线程阻塞 同步锁 网络编程 正则表达式 re.match函数 re.search方法 re.match与re.search的区别 re.findall()方法 正则表达式的特殊规则 闭包 account0 def atm(num,flag):global accountif flag:accountnumaccountprint(account)else:acco…

免费开源的高精度OCR文本提取,支持 100 多种语言、自动文本定位和脚本检测,几行代码即可实现离线使用(附源码)

免费开源的高精度OCR文本提取,支持 100 多种语言、自动文本定位和脚本检测,几行代码即可实现离线使用(附源码)。 要从图像、照片中提取文本吗?是否刚刚拍了讲义的照片并想将其转换为文本?那么您将需要一个可以通过 OCR(光学字符识别)识别文本的应用程序。 图片文字识…

html圣诞树代码

一、前言 想做一个圣诞树&#xff0c;通过html实现了下 二、效果展示 三、代码 <!DOCTYPE html> <html> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> <meta name"viewport" cont…

245. 你能回答这些问题吗——线段树

给定长度为 N 的数列 A&#xff0c;以及 M 条指令&#xff0c;每条指令可能是以下两种之一&#xff1a; 1 x y&#xff0c;查询区间 [x,y] 中的最大连续子段和&#xff0c; 2 x y&#xff0c;把 A[x] 改成 y。 对于每个查询指令&#xff0c;输出一个整数表示答案。 输入格式…

RabbitMQ 第二天 高级 9 RabbitMQ 集群搭建 9.3 集群管理 9.5 负载均衡-HAProxy

RabbitMQ 【黑马程序员RabbitMQ全套教程&#xff0c;rabbitmq消息中间件到实战】 文章目录RabbitMQ第二天 高级9 RabbitMQ 集群搭建9.3 集群管理9.5 负载均衡-HAProxy9.5.1 安装HAProxy9.5.2 配置HAProxy第二天 高级 9 RabbitMQ 集群搭建 9.3 集群管理 rabbitmqctl join_cl…

MariaDB上市:MySQL之父奋斗13年终敲钟 要写代码写到100岁

雷递网 雷建平 12月24日云数据库公司MariaDB日前与特殊目的公司Angel Pond Holdings完成合并&#xff0c;并在纽交所上市&#xff0c;新公司更名为MariaDB。MariaDB是2022年初与Angel Pond Holdings达成合并协议&#xff0c;对新公司的作价为6.72亿美元。MariaDB是MySQL之父Mic…

【技术应用】java基于UNIX域套接字(unix domain socket)连接redis

【技术应用】java基于UNIX域套接字unix domain socket连接redis一、前言二、实现思路三、代码实现1、java socket基于redis.sock连接redis2、Lettuce框架基于redis.sock连接redis一、前言 在公司工作中经常涉及到一些中小型项目&#xff0c;这些项目都会涉及使用redis数据库&a…

Redis5.0+——持久化——RDBAOF

Redis持久化-RDB 1.实现目标&#xff1a; 在redis持久化时&#xff0c;持久化dump.rdb文件放入到redis解压目录下的data目录下的6379目录下 2.前期准备 1.在redis-5.0.3解压目录下新建data数据目录 2.编辑前面配置的/etc/redis.conf配置文件 修改持久化文件位置 (1) 进入安…

MySQL热备之PXB备份与恢复

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

【语音处理】使用块反射器的基于DFT的系统中用于旁瓣抑制的正交预编码(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

Java中的多线程(下)

作者&#xff1a;~小明学编程 文章专栏&#xff1a;JavaEE 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 多线程案例 单例模式 饿汉模式 懒汉模式 阻塞式队列 为什么要引入阻塞队列 Java中的阻塞队列 模拟实现阻塞队列 定时器 标准库中的定时器 …

docker的虚悬镜像是什么?

虚悬镜像是什么? 答:仓库名、标签都是<none>的镜像,俗称:dangling image 我们使用Dockerfile写一个: 1:编写 from ubuntu CMD echo action is success2:构建 docker build . 注意没有 -t 产生原因: 1:构建时候因为编写错误导致 2:删除的时候 对于这样…

数据溢出的二进制原理

char 类型的数据占一个字节&#xff0c;一个字节有 8 位&#xff0c;最高位为符号位&#xff0c;1表示负数&#xff0c;0表示正数。在计算机中&#xff0c;数据用补码表示&#xff0c;正数的补码是它本身&#xff0c;负数的补码为 “符号位不变&#xff0c;其他位取反后再加1”…