ES6的代理模式 | Proxy

news2024/12/30 2:15:54

 🎬 岸边的风:个人主页

 🔥 个人专栏 :《 VUE 》 《 javaScript 》

⛺️ 生活的理想,就是为了理想的生活 !

在这里插入图片描述

 

目录

正文

语法

Handler 对象常用的方法

handler.get

可撤消的Proxy

Proxy的应用场景

校验器

私有属性

为什么要用Proxy重构

Vue中的defineProperty

对象新增属性为什么不更新

数组变异

对比


proxy修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

  • 元编程(英语:Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

一段代码来理解元编程

#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
    echo "echo $I" >>program
done
chmod +x program

这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行echo,如果我们手动来写1024行代码,效率显然低效

元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译

proxy 译为代理,可以理解为在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,生活中也有许许多多的“proxy”, 如代购,中介,因为他们所有的行为都不会直接触达到目标对象

正文

本篇文章作为 Vue3 源码系列前置篇章之一,Proxy 的科普文,跟Vue3并没有绝对关系,但是当你静下心读完了前置篇章,再去读后续的源码系列,感受定会截然不同

前置篇章包含

  • 为什么要学习源码
  • 认识Typescript
  • 理解函数式编程
  • 搞明白Proxy
  • 摸清楚Set、Map、WeakSet、WeakMap

下来将介绍 Proxy 的基本使用

语法

  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • handler 一个通常以函数作为属性的对象,用来定制拦截行为

const proxy = new Proxy(target, handle)

举个例子

const origin = {}
const obj = new Proxy(origin, {
  get: function (target, propKey, receiver) {
		return '10'
  }
});

obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

上方代码我们给一个空对象的get架设了一层代理,所有get操作都会直接返回我们定制的数字10,需要注意的是,代理只会对proxy对象生效,如上方的origin就没有任何效果

Handler 对象常用的方法

方法描述
handler.has()in 操作符的捕捉器。
handler.get()属性读取操作的捕捉器。
handler.set()属性设置操作的捕捉器。
handler.deleteProperty()delete 操作符的捕捉器。
handler.ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply()函数调用操作的捕捉器。
handler.construct()new 操作符的捕捉器

下面挑handler.get重点讲一下,其它方法的使用也都大同小异,不同的是参数的区别

handler.get

get我们在上面例子已经体验过了,现在详细介绍一下,用于代理目标对象的属性读取操作

授受三个参数 get(target, propKey, ?receiver)

  • target 目标对象
  • propkey 属性名
  • receiver Proxy 实例本身

举个例子

const person = {
  like: "vuejs"
}

const obj = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
})

obj.like // vuejs
obj.test // Uncaught ReferenceError: Prop name "test" does not exist.

上面的代码表示在读取代理目标的值时,如果有值则直接返回,没有值就抛出一个自定义的错误

注意:

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined

如下面的例子

const obj = {};
Object.defineProperty(obj, "a", { 
  configurable: false, 
  enumerable: false, 
  value: 10, 
  writable: false 
})

const p = new Proxy(obj, {
  get: function(target, prop) {
    return 20;
  }
})

p.a // Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable..

可撤消的Proxy

proxy有一个唯一的静态方法,Proxy.revocable(target, handler)

Proxy.revocable()方法可以用来创建一个可撤销的代理对象

该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}

  • proxy 表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉。
  • revoke 撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。

该方法常用于完全封闭对目标对象的访问, 如下示例

const target = { name: 'vuejs'}
const {proxy, revoke} = Proxy.revocable(target, handler)
proxy.name // 正常取值输出 vuejs
revoke() // 取值完成对proxy进行封闭,撤消代理
proxy.name // TypeError: Revoked

Proxy的应用场景

Proxy的应用范围很广,下方列举几个典型的应用场景

校验器

想要一个number,拿回来的却是string,惊不惊喜?意不意外?下面我们使用Proxy实现一个逻辑分离的数据格式验证器

嗯,真香!

const target = {
  _id: '1024',
  name:  'vuejs'
}

const validators = {  
    name(val) {
        return typeof val === 'string';
    },
    _id(val) {
        return typeof val === 'number' && val > 1024;
    }
}

const createValidator = (target, validator) => {
  return new Proxy(target, {
    _validator: validator,
    set(target, propkey, value, proxy){
      let validator = this._validator[propkey](value)
      if(validator){
        return Reflect.set(target, propkey, value, proxy)
      }else {
        throw Error(`Cannot set ${propkey} to ${value}. Invalid type.`)
      }
    }
  })
}

const proxy = createValidator(target, validators)

proxy.name = 'vue-js.com' // vue-js.com
proxy.name = 10086 // Uncaught Error: Cannot set name to 10086. Invalid type.
proxy._id = 1025 // 1025
proxy._id = 22  // Uncaught Error: Cannot set _id to 22. Invalid type 

私有属性

在日常编写代码的过程中,我们想定义一些私有属性,通常是在团队中进行约定,大家按照约定在变量名之前添加下划线 _ 或者其它格式来表明这是一个私有属性,但我们不能保证他能真私‘私有化’,下面使用Proxy轻松实现私有属性拦截

const target = {
  _id: '1024',
  name:  'vuejs'
}

const proxy = new Proxy(target, {
  get(target, propkey, proxy){
    if(propkey[0] === '_'){
      throw Error(`${propkey} is restricted`)
    }
    return Reflect.get(target, propkey, proxy)
  },
  set(target, propkey, value, proxy){
    if(propkey[0] === '_'){
      throw Error(`${propkey} is restricted`)
    }
    return Reflect.set(target, propkey, value, proxy)
  }
})

proxy.name // vuejs
proxy._id // Uncaught Error: _id is restricted
proxy._id = '1025' // Uncaught Error: _id is restricted

Proxy 使用场景还有很多很多,不再一一列举,如果你需要在某一个动作的生命周期内做一些特定的处理,那么Proxy 都是适合的

为什么要用Proxy重构

在 Proxy 之前,JavaScript 中就提供过 Object.defineProperty,允许对对象的 getter/setter 进行拦截

Vue3.0之前的双向绑定是由 defineProperty 实现, 在3.0重构为 Proxy,那么两者的区别究竟在哪里呢?

首先我们再来回顾一下它的定义

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

上面给两个词划了重点,对象上属性,我们可以理解为是针对对象上的某一个属性做处理的

语法

  • obj 要定义属性的对象
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符
Object.defineProperty(obj, prop, descriptor)

1

举个例子

const obj = {}
Object.defineProperty(obj, "a", {
  value : 1,
  writable : false, // 是否可写 
  configurable : false, // 是否可配置
  enumerable : false // 是否可枚举
})

// 上面给了三个false, 下面的相关操作就很容易理解了
obj.a = 2 // 无效
delete obj.a // 无效
for(key in obj){
  console.log(key) // 无效 
}

Vue中的defineProperty

Vue3之前的双向绑定都是通过 defineProperty 的 getter,setter 来实现的,我们先来体验一下 getter,setter

const obj = {};
Object.defineProperty(obj, 'a', {
  set(val) {
    console.log(`开始设置新值: ${val}`)
  },
  get() { 
    console.log(`开始读取属性`)
    return 1; 
  },
  writable : true
})

obj.a = 2 // 开始设置新值: 2
obj.a // 开始获取属性 

看到这里,我相信有些同学已经想到了实现双向绑定背后的流程了,其实很简单嘛,只要我们观察到对象属性的变更,再去通知更新视图就好了

我们摘抄一段 Vue 源码中的核心实现验证一下,这一部分一笔代过,不是本文重点

  // 源码位置:https://github.com/vuejs/vue/blob/ef56410a2c/src/core/observer/index.js#L135
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
      if (Dep.target) {
        // 收集依赖
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      // 通知视图更新
      dep.notify()
    }
  })

对象新增属性为什么不更新

这个问题用过Vue的同学应该有超过95%比例遇到过

data  () {
  return  {
    obj: {
      a: 1
    }
  }
}

methods: {
  update () {
    this.obj.b = 2
  }
}

上面的伪代码,当我们执行 update 更新 obj 时,我们预期视图是要随之更新的,实际是并不会

这个其实很好理解,我们先要明白 vue 中 data init 的时机,data init 是在生命周期 created 之前的操作,会对 data 绑定一个观察者 Observer,之后 data 中的字段更新都会通知依赖收集器Dep触发视图更新

然后我们回到 defineProperty 本身,是对对象上的属性做操作,而非对象本身

一句话来说就是,在 Observer data 时,新增属性并不存在,自然就不会有 getter, setter,也就解释了为什么新增视图不更新,解决有很多种,Vue 提供的全局$set 本质也是给新增的属性手动 observer

// 源码位置 https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L201
function set (target: Array<any> | Object, key: any, val: any): any {
  // ....
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

数组变异

由于 JavaScript 的限制,Vue 不能检测以下数组的变动: 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

先来看一段代码

var vm = new Vue({
  data: {
    items: ['1', '2', '3']
  }
})
vm.items[1] = '4' // 视图并未更新

文档已经做出了解释,但并不是defineProperty的锅,而是尤大在设计上对性能的权衡,下面这段代码可以验证

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
      get: function defineGet() {
        console.log(`get key: ${key} val: ${val}`);
        return val;
      },
      set: function defineSet(newVal) {
        console.log(`set key: ${key} val: ${newVal}`);
        val = newVal;
      }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  })
}

let test = [1, 2, 3];

observe(test);

test[0] = 4 // set key: 0 val: 4

虽然说索引变更不是 defineProperty 的锅,但新增索引的确是 defineProperty 做不到的,所以就有了数组的变异方法

能看到这里,大概也能猜到内部实现了,还是跟$set一样,手动 observer,下面我们验证一下

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // 缓存原生数组
  const original = arrayProto[method]
  // def使用Object.defineProperty重新定义属性
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args) // 调用原生数组的方法

    const ob = this.__ob__  // ob就是observe实例observe才能响应式
    let inserted
    switch (method) {
      // push和unshift方法会增加数组的索引,但是新增的索引位需要手动observe的
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 同理,splice的第三个参数,为新增的值,也需要手动observe
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 其余的方法都是在原有的索引上更新,初始化的时候已经observe过了
    if (inserted) ob.observeArray(inserted)
    // dep通知所有的订阅者触发回调
    ob.dep.notify()
    return result
  })
})

对比

一个优秀的开源框架本身就是一个不断打碎重朔的过程,上面做了些许铺垫,现在我们简要总结一下

  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化

  • Proxy 能观察的类型比 defineProperty 更丰富

  • Proxy 不兼容IE,也没有 polyfilldefineProperty 能支持到IE9

  • Object.definedProperty 是劫持对象的属性,新增元素需要再次 definedProperty。而 Proxy 劫持的是整个对象,不需要做特殊处理

  • 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截

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

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

相关文章

nacos安装和入门

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 一、Nacos在mac环境的服务搭建 1、首先进入Nacos官网&#xff0c;链接 2、点击前往Github&#xff0c;点击红色链接。 3、选择相应版本下载。 4、下载之后解压。 5、在终端执行以下命令启动Nacos…

为什么 InfiniBand 网络在高性能计算数据中心如此重要?

随着数据分析和机器学习等高数据吞吐量应用的快速扩展&#xff0c;对高带宽和低延迟互连的需求也扩展到更广阔的市场。为更好应对这一需求&#xff0c;越来越多的高性能计算数据中心选择采用InfiniBand技术。相较于以太网&#xff0c;InfiniBand是一种专为高速互连而设计的网络…

使用jupyter运行sympy库

1.首先下载sympy插件&#xff1a; &#xff08;1&#xff09;在下图这里输入cmd (2)pip install sympy 若出现不信任此资源错误&#xff0c;改成pip install sympy--trusted-host pypi.tuna.tsinghua.edu.cn (3)检查是否安装成功 python import sympy sympy.__version__&#…

PCB走线规则

1、线间距。 这里应该遵循3W规则&#xff0c;所谓3W就是为了减少线间串扰&#xff0c;应保证线间距足够大&#xff0c;当线中心不少于3倍线宽&#xff0c;则可 保持70%的电场不互相干扰。如要达到98%的电场不互相干扰&#xff0c;可使用10W的间距。——这是查阅华为PCB布线规则…

UDP与TCP报头介绍,三次握手与四次挥手详谈

先介绍我们UDP/TCP协议缓冲区 在UDP和TCP在数据传输和介绍时有有缓冲区概念的。 UDP缓冲区 UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后 续的传输动作; UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序…

repmgr管理pg高可用

repmgr用于管理 PostgreSQL 服务器集群内的复制和故障转移&#xff0c;类似dg-broker&#xff0c;可以实现swichover和failover。 整体架构 repmgr架构图如下&#xff1a; 每个节点都可通过 repmgrd 守护进程来监控节点数据库状态&#xff1b;每个节点元数据表可独立维护&…

Stream流处理快速上手最佳实践 | 京东物流技术团队

一 引言 JAVA1.8得益于Lambda所带来的函数式编程&#xff0c;引入了一个全新的Stream流概念Stream流式思想类似于工厂车间的“生产流水线”&#xff0c;Stream流不是一种数据结构&#xff0c;不保存数据&#xff0c;而是对数据进行加工处理。Stream可以看作是流水线上的一个工…

深度解析NLP文本摘要技术:定义、应用与PyTorch实战

目录 1. 概述1.1 什么是文本摘要&#xff1f;1.2 为什么需要文本摘要&#xff1f; 2. 发展历程2.1 早期技术2.2 统计方法的崛起2.3 深度学习的应用2.4 文本摘要的演变趋势 3. 主要任务3.1 单文档摘要3.2 多文档摘要3.3 信息性摘要 vs. 背景摘要3.4 实时摘要 4. 主要类型4.1 抽取…

【Linux进行时】进程状态

进程状态&#xff1a; ❓假设我们在上课&#xff0c;在B站上上课&#xff0c;请问我们的B站是不是一直运行呢&#xff1f;&#x1f4a1;不是的&#xff01; ❓假设我们同时打开了B站和PDF阅读器时&#xff0c;是怎么运行的呢&#xff1f; &#x1f4a1;每一个进程在CPU跑一会&a…

Layui快速入门之第十三节 日期与时间选择器

目录 一&#xff1a;基本用法 API 渲染 属性 弹出提示 2.8 获取实例 2.8 解除实例绑定 2.8 关闭日期面板 2.7 获取某月的最后一天 二&#xff1a;常规用法 三&#xff1a;多类型选择器 四&#xff1a;范围选择 五&#xff1a;直接静态显示 六&#xff1a;更多功能…

Vue入门--vue的生命周期

一.什么是Vue 二.Vue的简介 官方网址 特点 三. 前后端的分离 重大问题 优势 4.Vue入门 定义一个管理边界 ​编辑 测试结果 vue的优势 ​编辑 测试结果 5.Vue的生命周期 vue的生命周期图 ​编辑建立一个html 测试结果 一.什么是Vue Vue是一种流行的JavaScript前端框…

华为云云耀云服务器L实例评测|使用宝塔10分钟部署一个围猫猫小游戏

目录 前言一、选择华为云云耀云服务器L实例的原因二、华为云云耀云服务器的优势三、快速部署一个小游戏&#xff08;1&#xff09;终端部署1、使用Termius工具连接终端2、安装Nginx3、上传打包文件 &#xff08;2&#xff09;宝塔可视化面板部署1、进入宝塔2、宝塔菜单3、上传代…

【实训项目】你好,教练-校园私教平台的设计与开发

1.设计摘要 随着社会的进步&#xff0c;人们的健康意识逐渐提高&#xff0c;越来越多的人选择在闲暇时间健身&#xff0c;在大学生群体中&#xff0c;这一现象犹为明显。在大学城内&#xff0c;有多家健身房供同学选择&#xff0c;也有许多同学选择在操场或者宿舍内自己健身&a…

使用Seata实现分布式事务

Seata 一&#xff1a;故事背景二&#xff1a;使用方法2.1 下载安装Seata2.4 修改对应配置文件。2.4.1 配置中心2.4.1 注册中心2.4.2 日志保存模式 2.3 启动Seata2.4 项目中集成2.5 数据库内新建undo_log 表进行日志记录2.6 编写代码测试Seata提供的分布式事务功能 三&#xff1…

带你熟练使用list

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

EdgeX Foundry MQTT设备服务

一、部署edgex 1.运行命令行&#xff0c;进入放置docker-compose-fuji-no-secty.yml文件的路径下 下载到本地的docker-compose文件 执行&#xff1a; docker-compose -f docker-compose-fuji-no-secty.yml pull 拉取相关镜像&#xff0c;这里默认将文件中没有注释的服务镜像全部…

企业注册版 :ag-Grid Enterprise 30.1.0

Ag-Grid 被描述为一种商业产品&#xff0c;已根据 EULA 进行分发&#xff0c;并受到我们提供的技术团队的鼓励。它非常高级&#xff0c;具有与行分组一样的性能以及范围选择、主数据和案例、行的服务器端模型等等。Ag-Grid Enterprise 通常附带集成图表&#xff0c;允许用户和开…

无涯教程-JavaScript - IFS函数

描述 IFS函数检查是否满足一个或多个条件,并返回与第一个TRUE条件相对应的值。此功能已在Excel 2016中添加。 语法 IFS (logical_test1, value_if_true1, [logical_test2, value_if_true2], [logical_test3, value_if_true3]…) 争论 Argument描述Required/Optionallogical…

无涯教程-JavaScript - ATAN函数

描述 The ATAN function returns the arctangent, or inverse tangent, of a number. The returned angle is given in radians between -π/2 and π/2. The arctangent is the angle whose tangent is number. 语法 ATAN (number)争论 Argument描述Required/OptionalNumb…

springboot配置注入增强(二)属性注入的原理

一 原理 1 配置的存储 springboot在启动的时候会后构建一个org.springframework.core.env.Environment类型的对象&#xff0c;这个对象就是用于存储配置&#xff0c;如图springboot会在启动的最开始创建一个Environment对象 这个webApplicationType的枚举是在new SpringAppli…