理解 Proxy 和 Reflect

2024-03-28 08:10
文章标签 理解 proxy reflect

本文主要是介绍理解 Proxy 和 Reflect,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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的相关知识,提升工作效率。



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

这篇关于理解 Proxy 和 Reflect的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/855002

相关文章

回调的简单理解

之前一直不太明白回调的用法,现在简单的理解下 就按这张slidingmenu来说,主界面为Activity界面,而旁边的菜单为fragment界面。1.现在通过主界面的slidingmenu按钮来点开旁边的菜单功能并且选中”区县“选项(到这里就可以理解为A类调用B类里面的c方法)。2.通过触发“区县”的选项使得主界面跳转到“区县”相关的新闻列表界面中(到这里就可以理解为B类调用A类中的d方法

如何理解redis是单线程的

写在文章开头 在面试时我们经常会问到这样一道题 你刚刚说redis是单线程的,那你能不能告诉我它是如何基于单个线程完成指令接收与连接接入的? 这时候我们经常会得到沉默,所以对于这道题,笔者会直接通过3.0.0源码分析的角度来剖析一下redis单线程的设计与实现。 Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源

MySQL理解-下载-安装

MySQL理解: mysql:是一种关系型数据库管理系统。 下载: 进入官网MySQLhttps://www.mysql.com/  找到download 滑动到最下方:有一个开源社区版的链接地址: 然后就下载完成了 安装: 双击: 一直next 一直next这一步: 一直next到这里: 等待加载完成: 一直下一步到这里

PyTorch模型_trace实战:深入理解与应用

pytorch使用trace模型 1、使用trace生成torchscript模型2、使用trace的模型预测 1、使用trace生成torchscript模型 def save_trace(model, input, save_path):traced_script_model = torch.jit.trace(model, input)<

isa指针的理解

D3实例isa指向D3类对象。D3类的话isa指向D3元类对象。D3元类保存类中的方法调度列表,包括类方法和对象方法

WeakHashMap深入理解

这一章,我们对WeakHashMap进行学习。 我们先对WeakHashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用WeakHashMap。 第1部分 WeakHashMap介绍 第2部分 WeakHashMap数据结构 第3部分 WeakHashMap源码解析(基于JDK1.6.0_45) 第4部分 WeakHashMap遍历方式 第5部分 WeakHashMap示例

netty中常用概念的理解

目录   目录ChannelHandler ChannelHandler功能介绍通过ChannelHandlerAdapter自定义拦截器ChannelHandlerContext接口ChannelPipeline ChannelPipeline介绍ChannelPipeline工作原理ChannelHandler的执行顺序   在《Netty权威指南》(第二版)中,ChannelP

安全科普:理解SSL(https)中的对称加密与非对称加密

今天刚好为站点的后台弄了下https,就来分享我了解的吧。 密码学最早可以追溯到古希腊罗马时代,那时的加密方法很简单:替换字母。 早期的密码学:   古希腊人用一种叫 Scytale 的工具加密。更快的工具是 transposition cipher—:只是把羊皮纸卷在一根圆木上,写下信息,羊皮纸展开后,这些信息就加密完成了。 虽然很容易被解密,但它确实是第一个在现实中应用加密的

java同步锁以及级别升级的理解

首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景: 偏向锁:只有一个线程进入临界区;轻量级锁:多个线程交替进入临界区;重量级锁:多个线程同时进入临界区。 还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:   synchronized (lockObject) { // do something } 上述同步代码块

理解什么是DSR,嗅探器视角下的IP和MAC地址识别(C/C++代码实现)

网络嗅探器是监控和分析网络流量的一种工具,它能够捕获数据包并提取出关键的信息,比如IP地址和MAC地址。 网络嗅探器工作原理基于网卡的工作模式。正常情况下,网卡只处理发送给它的数据包,忽略其他数据。但是,如果将网卡设置为“混杂模式”,那么它可以接收到网络上所有的数据包,而不仅仅是发给它的数据包。网络嗅探器就是利用了这一特性来捕获网络上的数据交换。 数据包是网络通信的基本单位,包含了传输数据和控