JavaScript高级语法笔记(四):Proxy和响应式原理

128 阅读8分钟

Proxy和响应式原理

Proxy

  • 概念和语法

    • Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

    • Proxy的语法格式如下

      /**
      * target表示的就是要拦截(代理)的目标对象;而handler是用来定制拦截行为
      * 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值
      */
      const proxy = new Proxy(target, handler)
      

      可以将Proxy理解成“拦截”,在目标对象之前架设一层“拦截”,当外界对该对象的访问,都必须先通过这层拦截,正因为有了一种拦截机制,当外界的访问我们可以对进行一些操作(过滤或改写)

  • handler中的方法

    • get

      • get方法可自动接受3个参数target, propKey, receiver,分别表示要代理的目标对象、对象上的属性以及代理对象,该方法用于拦截某个属性的读取操作,比如proxy.fooproxy['foo']
    • set

      • set方法可自动接受4个参数:target, propKey, value, receiver,分别表示要代理的目标对象、对象上的属性、属性对应的值以及代理对象。该方法用于拦截对象属性操作,像proxy.foo = xxxproxy['foo'] = xxx
    • has

      • has方法接受target, propKey,用于拦截propKey in proxy的操作,返回一个布尔值,表示属性是否存在。
    • deleteProperty

      • 可接收target, propKey,用于拦截delete操作,返回一个布尔值,表示是否删除成功。
    • ownKeys

      • 可接收target,用于拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环等类似操作,返回一个数组,表示对象所拥有的keys。
    • getOwnPropertyDescriptor

      • 接收target和propKey,用于拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
    • defineProperty

      • 接收target, propKey, propDesc,分别表示目标对象、目标对象的属性,以及属性描述配置,用于拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs)的操作。
    • preventExtensions

      • 可接收target,用于拦截Object.preventExtensions(proxy)操作,补充说明一下preventExtensions的作用是将一个对象变成不可扩展,也就是永远不能再添加新的属性。
    • getPrototypeOf(target)

      • 在使用Object.getPrototypeOf(proxy)会触发调用,返回一个对象。
    • isExtensible(target)

      • 当使用Object.isExtensible(proxy)时会触发调用,返回一个布尔值,表示是否可扩展。
    • setPrototypeOf(target, proto)

      • 当调用Object.setPrototypeOf(proxy, proto)会触发该函数调用。
    • apply(target, object, args)

      • 接收三个参数target, object, args,分别表示目标对象、调用函数是的this指向以及参数列表,当Proxy实例作为函数调用时触发,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
    • construct(target, args)

      • 接收target和args,表示目标函数即参数列表,当Proxy 实例作为构造函数时触发该函数调用,比如new proxy(...args)
  • 总结

    • 代理对象不等于目标对象,他是目标对象的包装品
    • 目标对象既可以直接操作,也可以被代理对象操作,且两者相互关联
    • 如果直接操作目标对象,则会绕过代理定义的各种拦截行为
    • 如果用了代理,那肯定是希望给对象的操作嵌入我们定义的特殊行为,所以一般就操作代理对象就好

Reflect

  • 概述

    Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新的API。Reflect对象设计的目的主要有以下几个:

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

    • 修改某些 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
      }
      
    • 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name],而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为。

      // 老写法
      'assign' in Object // true
      ​
      ​
      // 新写法
      Reflect.has(Object, 'assign') // true
      
    • Reflect对象的方法与 Proxy对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,我们总可以在 Reflect 上获取默认行为。

      Proxy(target, {
        set: function(target, name, value, receiver) {
          var success = Reflect.set(target, name, value, receiver);
          if (success) {
            console.log('property ' + name + ' on ' + target + ' set to ' + value);
          }
          return success;
        }
      });
      

      上面代码中, Proxy 方法拦截 target 对象的属性赋值行为。它采用 Reflect.set 方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

      下面是另一个例子。

      var loggedObj = new Proxy(obj, {
        get(target, name) {
          console.log('get', target, name);
          return Reflect.get(target, name);
        },
        deleteProperty(target, name) {
          console.log('delete' + name);
          return Reflect.deleteProperty(target, name);
        },
        has(target, name) {
          console.log('has' + name);
          return Reflect.has(target, name);
        }
      });
      

      上面代码中,每一个 Proxy 对象的拦截操作( get 、 delete 、 has ),内部都调用对应的 Reflect 方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。

      有了 Reflect 对象以后,很多操作会更易读。

      // 老写法
      Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
      ​
      ​
      // 新写法
      Reflect.apply(Math.floor, undefined, [1.75]) // 1
      
  • 静态方法

    Reflect对象一共有 13 个静态方法(匹配Proxy的13种拦截行为)。

    • Reflect.apply(target, thisArg, args)
    • Reflect.construct(target, args)
    • Reflect.get(target, name, receiver)
    • Reflect.set(target, name, value, receiver)
    • Reflect.defineProperty(target, name, desc)
    • Reflect.deleteProperty(target, name)
    • Reflect.has(target, name)
    • Reflect.ownKeys(target)
    • Reflect.isExtensible(target)
    • Reflect.preventExtensions(target)
    • Reflect.getOwnPropertyDescriptor(target, name)
    • Reflect.getPrototypeOf(target)
    • Reflect.setPrototypeOf(target, prototype)- 用途

    先看一个复杂的例子。

    let user = {
        _name: "张三",
        get name() {
            return this._name;
        }
    };
    ​
    let userProxy = new Proxy(user, {
        get(target, prop, receiver) {
            return Reflect.get(target, prop);
            // return target[prop]; // (*) target = user
        }
    });
    ​
    let admin = {
        __proto__: userProxy,
        _name: "李四"
    };
    ​
    // except 李四
    console.log(admin.name); // 张三
    

    上述的情况可以看到无论是使用 Reflect.get(target, prop) 还是 target[prop], 都是张三。如何在这种情况下,正确的传递上下文,是个问题。如果是普通的函数的话,我们还可以通过 call/apply,但在这里我们是 getter,而不是调用。

    get(target, prop, receiver) 有第三个参数没有使用,而且我们也知道 Reflect 的方法的参数是和 Proxy handler 一致的,那我们试试将receiver传递进入Reflect.get,看修改以后的效果。

    let user = {
        _name: "张三",
        get name() {
            return this._name;
        }
    };
    ​
    let userProxy = new Proxy(user, {
        get(target, prop, receiver) { // receiver = admin
            return Reflect.get(target, prop, receiver);
        }
    });
    ​
    let admin = {
        __proto__: userProxy,
        _name: "李四"
    };
    ​
    console.log(admin.name); // => 李四
    

    可以看到第三个参数 receiver 保持了正确的 this 引用,在示例中,指向了 admin。在复杂的使用场景保持正确的上下文,这是 Reflect 一系列 API的一个重要意义所在。

    基于发布—订阅模式的响应式原理

  • 发布—订阅模式

    • 如下图,可以看到有三个主要元素

      • 发布者

      • 订阅者

      • 消息中心

image.png - 发布和订阅都是跟消息中心通信,从而达到解耦。

-   vue3中的响应式:`reactive`包装数据,`effect`定义数据变化后的回调。
  • 基于Object.defineProperty的响应式

    • Object.defineProperty的作用

      • 劫持一个对象的属性,通常我们对属性的gettersetter方法进行劫持,在对象的属性发生变化时进行特定的操作。
    • Object.defineProperty的缺陷

      • 无法监听数组变化。
      • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。
    • Proxy在ES2015规范中被正式发布,可以这样认为,Proxy是Object.defineProperty的全方位加强版。

  • 基于Proxy实现响应式

    • reactive():为目标对象创建一个Proxy对象(代理对象)。

      function reactive(target) {
        return new Proxy(target, {
          get(target, key, receiver) {
            //订阅
            track(target, key);
            return Reflect.get(target, key, receiver);
          },
          set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);
            // 发布
            trigger(target, key);
            return result;
          },
        });
      }
      
    • effect():注册副作用函数机制

      // 用一个全局变量储存被注册的副作用函数
      let activeEffect;
      // 用于注册副作用函数
      function effect(fn) {
        const effectFn = () => {
          activeEffect = effectFn;
          fn();
        };
        // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
        effectFn.deps = [];
        effectFn();
        fn();
      }
      
    • track():订阅函数

      // 订阅:将依赖存入bucket(消息中心)
      const bucket = new WeakMap();
      function track(target, key) {
        if (!activeEffect) return;
        // 根据target从bucket中取的desMap,没有则新建一个与target关联
        let depsMap = bucket.get(target);
        if (!depsMap) {
          bucket.set(target, (depsMap = new Map()));
        }
        // 再根据key从depsMap中取得deps,它是一个Set类型,里面存储着所有与当前key相关连的依赖
        let deps = depsMap.get(key);
        if (!deps) {
          depsMap.set(key, (deps = new Set()));
        }
        // 把当前激活的副作用函数添加到依赖集合deps中
        deps.add(activeEffect);
        // deps就是一个与当前副作用函数存在联系的依赖集合,将其添加到activeEffect.deps中
        activeEffect.deps.push(deps);
      }
      
    • trigger():发布函数

      // 发布:当侦听到对应数据变化并且在bucket中能找到相应的回调函数时,执行即可
      function trigger(target, key) {
        const depsMap = bucket.get(target);
        if (!depsMap) return;
        const effects = depsMap.get(key);
        effects && effects.forEach(effectFn => effectFn())
      }