通过写 on-change 库理解 JavaScript Proxy

发布于 2023-05-04 20:06:48 字数 8675 浏览 70 评论 0

JavaScript 中的 Proxy 是 ES6 的新语法,可以利用这个强大的特性来优雅的解决各种问题。这篇文章里通过写一个 on-change 库从而理解 Proxy。所以 on-change 这个库是用来做什么用的?这是一个为了观察 object 或 array 改变的库。来看一个简单的例子:

const onChange = require('on-change');
const object = {
  foo: false,
  a: {
    b: [{
      c: false
    }]
  }
};
let i = 0;

const logger = () => console.log('Object changed:', ++i);
const watchedObject = onChange(object, logger);

watchedObject.foo = true; //=> 'Object changed: 1'
watchedObject.a.b[0].c = true; //=> 'Object changed: 2'

有几个需要注意的点:

  • onChange 是一个接收两个参数的方法:需要观察的对象和当对象改变时运行的回调函数,最后返回改变后的对象。
  • 在代码第 17 行,当定义 foo 为 true 时,回调函数 logger 被执行。
  • 在代码第 20 行,当定义一个多嵌套的对象时,即便是数组,回调函数 logger 会被执行。这说明 onchange 是递归检查执行,所以修改多嵌套里的属性也会触发回调。

所以现在看看应该如何使用 JS 中 Proxy 语法来重新创建这个 on-change 工具库,首先需要学习 Proxy 的基础语法使用。

什么是 Proxy ?

思考以下这段代码

const someObject = { prop1: 'Awesome' };

console.log(someObject.prop1);  // Awesome

console.log(someObject.prop2);  // undefined

someObject.prop1 输出 Awesome,someObject.prop2 输出 undefined 因为 prop2 这个属性不存在。假设当访问一个不存在的值时就返回其默认值,在这里访问 someObject.prop2 应该返回 噢上帝,你访问的属性不存在 而不是 undefined。怎样如何实现而无需在 someObject 修改或添加新属性?

此刻 Proxy 登场。在牛津英文词典中 proxy 这词代表的意思是代表其他人的权利。这正是 JS 中的 Proxy。Proxy 是 ES6 中的一部分,通过 Proxy 可以创建一个代理,用于替代另一个对象(目标),这个代理对目标对象进行了虚拟。Proxy 允许我们对 object 进行拦截一些操作(如设置值或删除值)的执行。当访问一个对象属性时,发生了如下图所示的事:

当使用 Proxy 时,流程发生了一些变化:

从上图可看出,Proxy 位置在 Object 与 Program 中间调解值的改变。Proxy 会检查 Object 的属性值,如果不存在那也可以返回响应。现在来看看如何在 JS 中创建,不过在此之前应该先熟悉以下术语:

  • target 目标对象
  • traps 陷阱,意为将要拦截的操作。例如,在对象里访问一个属性称为 get traps,设置一个属性值称为 set traps,删除一个属性值称为 deleteProperty trap。还有许多 traps 可查阅这里
  • handler 处理器 一个包含所有 traps 的对象,包含它的描述信息

创建 Proxy 对象

首先要做的就是创建一个需要被观察的 object:

const originalObject = { firstName: 'Arfat', lastName: 'Salman' };

现在需要考虑什么对象操作是需要被 traps 拦截的。这里首先添加了一个 get traps,这个 trap 将会在 handler 中注册:

const handler = {
  get(target, property, receiver) {
    console.log(`GET ${property}`);
    return target[property];
  }
};

有几个需要注意的点:

  • handler 是一个普通的对象
  • traps 函数是 handler 的一部分。traps 的名字是固定不变且被预先定义的
  • get trap 接收三个参数:target, property, receiver
  • target 将接收属性的对象(即代理的目标对象)
  • property 需要读取的属性的键值
  • receiver 操作发生的对象(通常是代理对象)

现在我们使用 Proxy 构造函数结合 handler 和 originalObject

const proxiedObject = new Proxy(originalObject, handler);

整段代码如下

const originalObject = { firstName: 'Arfat', lastName: 'Salman' };

const handler = {
  get(target, property, receiver) {
    console.log(`GET ${property}`);
    return target[property];
  }
};

const proxiedObject = new Proxy(originalObject, handler);

如果浏览器支持 Proxy 语法,那么可以运行在 console 中,或直接使用命令行 node 运行。如果直接打印出 proxiedObject.firstName 属性那么将会输出

console.log(proxiedObject.firstName);
//=> GET firstName
//=> Arfat

现在来修改 handler 来处理不存在属性的情况

const handler = {
  get(target, property, receiver) {
    console.log(`GET ${property}`)

    if (property in receiver) return target[property]
    return '噢上帝,你访问的属性不存在'
  }
};

当 property 属性存在 receiver 对象时则说明该属性是存在于对象里的,否则返回提示。现在当你访问 proxiedObject.thisPropertDoesNotExist 将会返回

console.log(proxiedObject.thisPropertDoesNotExist);
// => GET thisPropertyDoesNotExist
// => 噢上帝,你访问的属性不存在

这次将不会再返回 undefined 而是自定义的返回消息。需要注意的是,如果没有定义任何 traps,则正常的传值就像 proxy 不存在。

重新写一个 on-change 库

理解了 proxy 基础语法和如何工作后,我们将重写 on-change 这个库。正如文章开头部分所描述,onChange 是一个接收两个参数的函数:需要观察的对象和当对象改变时运行的回调函数。第一步先创建一个函数:

const onChange = (objToWatch, onChangeFunction) => {}

首先梳理下目前需要做的:希望每当 objToWatch 对象改变时都将执行 onChangeFunction。也就是说当一个属性被访问或修改,或新添加一个属性,或删除一个属性的情况。

在 onChange 函数中返回一个空 handler 的 proxy 对象。因为没有定义 traps 所以任何操作都是传值到目标对象。

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {};
  return new Proxy(objToWatch, handler);
};

因为理解了 get trap 所以首先将注意力放在 当一个属性被访问或修改 这个条件上。当返回属性值时将执行 onChangeFunction,于是写出下面的代码:

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction();
      return target[property];
    }
  };
  return new Proxy(objToWatch, handler);
};

现在将注意力放在 添加或删除一个属性 上。当设置一个属性值或者修改值时触发的是 set trap:

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction();
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      onChangeFunction();
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(objToWatch, handler);
};

注意这里改写并使用 Reflect API(Proxy 里的 trap 也推荐使用 Reflect)。Reflect.get() 是 get trap 对应的反射方法,同时也是 set 操作的默认行为。Reflect.set() 同理。

Reflect 对象所代表的是反射接口,是给底层操作提供默认行为的方法的集合,这些操作是能够被代理重写的。每个代理 trap 都有一个对应的反射方法,每个方法都与对应的 trap 函数同名,并且接收的参数也与之一致。下表总结了这些行为:

代理 trap被重写的行为默认行为
get读取一个属性的值Reflect.get()
set写入一个属性Reflect.set()
hasin 运算符Reflect.has()
deletePropertydelete 运算符Reflect.deleteProperty()
getPrototypeOfObject.getPrototypeOf()Reflect.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()Reflect.setPrototypeOf()
isExtensibleObject.isExtensible()Reflect.isExtensible()
preventExtensionsObject.preventExtensions()Reflect.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()Reflect.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()Reflect.defineProperty
ownKeysObject.keys 、Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols()Reflect.ownKeys()
apply调用一个函数Reflect.apply()
construct使用 new 调用一个函数Reflect.construct()

可以点击这里阅读了解为什么最好使用 Reflect API。通过查阅表了解到 deleteProperty trap 则是使用 Reflect.deleteProperty(),既可添加 deleteProperty 完成最后一个触发条件 删除一个属性:

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction();
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value) {
      onChangeFunction();
      return Reflect.set(target, property, value);
    },
    deleteProperty(target, property) {
      onChangeFunction();
      return Reflect.deleteProperty(target, property);
    }
  };
  return new Proxy(objToWatch, handler);
};

到此 on-change 的改写就完成的差不多了,可以通过 console 测试:

const logger = () => console.log('我被执行');
const obj = { a: 'a' };
const proxy = onChange(obj, logger);

console.log(proxy.a); // 我被执行 in get trap
proxy.b = 'b'; // 我被执行 in set trap
delete proxy.a; // 我被执行 in deleteProperty trap

可看到控制台将输出三次的 "我被执行"。但是如果 obj 为数组且数组中包含一个对象的话,那么修改这个数组里的对象将不会触发 logger 函数。举个例子 [1, 2, {a: false}] 当设置 array[2].a = true 将不会触发 logger。

可以通过简单的方式来处理这个 bug,通过判断 Reflect.get() 的返回值,如果值为 object 则返回一个新的 proxy:

get(target, property, receiver) {
  onChangeFunction();
  const value = Reflect.get(target, property, receiver);
  if (typeof value === 'object') {
    return new Proxy(value, handler);
  }
  return value;
}

这样 onChange 便可观察 array 与 object 的变化。是不是对 proxy 认知更清晰了呢?这篇文章涉及的只是 proxy 的基础知识,更深入扩展建议阅读 understanding ecmascript 6 第十二章 proxy 篇。

原文:https://codeburst.io/understanding-javascript-proxies-by-examining-on-change-library-f252eddf76c2

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

佞臣

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

qq_eQNo9e

文章 0 评论 0

内心旳酸楚

文章 0 评论 0

mb_BlPo2I8v

文章 0 评论 0

alipaysp_ZRaVhH1Dn

文章 0 评论 0

alipaysp_VP2a8Q4rgx

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文