使用 Javascript 原生的 Proxy 优化应用

看到 Proxy就应该想到代理模式(Proxy Pattern)Proxy 是 Javascript ES2015 标准的一部分,咱们应该学会使用它,代理模式是一种设计模式,使用 Proxy 对象能够垂手可得的在 Javascript 中建立代理模式。然而,使用设计模式并非目的,目的在于解决实际问题。本文首先会简单介绍 Proxy 的基本用法,接着将会叙述如何使用 Proxy 建立代理模式而且对咱们的应用进行优化。javascript


Proxy 的基本使用

开始学习 Proxy 的使用以前,建议首先对 Reflect 有必定的了解,若是很陌生的话,建议先花 1 分钟浏览相关知识。java

好了,如今假设已经具有了必定的 Reflect 知识,就开始掌握 Proxy 吧。算法

基本语法

Proxy 相关的方法一共就两个:设计模式

  • 构造方法 本文着重讨论
  • Proxy.revocable() 建立一个可撤销的 Proxy 对象,其他与构造函数相似,理解了 Proxy 的构造方法后,该方法与构造方法使用很是相似,本文再也不涉及

接下来本文将围绕 Proxy 的构造方法进行讲解。数组

let p = new Proxy(target, handler);
复制代码

参数promise

  • target 任何类型的对象,包括原生数组,函数,甚至另外一个 Proxy 对象缓存

  • handler 一个对象,其属性是当执行一个操做时定义代理的行为的函数, 容许的属性一共 13 种,与 Reflect 的方法名一致bash

返回网络

  • p Proxy 对象

注意: new Proxy 是稳定操做,不会对 target 有任何影响。app

下面来看几个表明性的例子,便于加深理解。

代理一个对象字面量:

const target = {};
const handler = {
  set: (obj, prop, value) => {
    obj[prop] = 2 * value;
  },
  get: (obj, prop) => {
    return obj[prop] * 2;
  }
};

const p = new Proxy(target, handler);

p.x = 1;          // 使用了 set 方法
console.log(p.x); // 4, 使用了 get 方法
复制代码

代理一个数组:

const p = new Proxy(
  ['Adela', 'Melyna', 'Lesley'],
  {
    get: (obj, prop) => {
      if (prop === 'length') return `Length is ${obj[prop]}.`;
      return `Hello, ${obj[prop]}!`;
    }
  }
);

console.log(p.length) // Length is 3.
console.log(p[0]); // Hello, Adela
console.log(p[1]); // Hello, Melyna
console.log(p[2]); // Hello, Lesley
复制代码

代理一个普通函数:

const foo = (a, b, c) => {
  return a + b + c;
}
const pFoo = new Proxy(foo, {
  apply: (target, that, args) => {
    const grow = args.map(x => x * 2);
    const inter = Reflect.apply(target, that, grow);
    return inter * 3;
  }
});

pFoo(1, 2, 3);   // 36, (1 * 2 + 2 * 2 + 3 * 2) * 3 
复制代码

代理构造函数

class Bar {
  constructor(x) {
    this.x = x;
  }
  say() {
    console.log(`Hello, x = ${this.x}`);
  }
}
const PBar = new Proxy(Bar, {
  construct: (target, args) => {
    const obj = new Bar(args[0] * 2);
    return obj;
  }
});

const p = new PBar(1);
p.say(); // Hello, x = 2
复制代码

Proxy 的基本用法无出其上,可 Proxy 的真正用途尚未显现出来,接下来结合设计模式中的一种模式 —— 代理模式 —— 进一步讨论。


使用 Proxy 建立代理模式

从上面的例子并不能看出 Proxy 给咱们带来了什么便利,须要实现的功能彻底能够在原函数内部进行实现。既然如此,使用代理模式的意义是什么呢?

  • 遵循“单一职责原则”,面向对象设计中鼓励将不一样的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念
  • 遵循“开放-封闭原则”,代理能够随时从程序中去掉,而不用对其余部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种缘由再也不须要代理,那么就能够容易的将代理对象换成原对象的调用

达到上述两个原则有一个前提就是代理必须符合“代理和本体接口一致性”原则:代理和原对象的输入和输出必须是一致的。这样对于用户来讲,代理就是透明的,代理和原对象在不改动其余代码的条件下是能够被相互替换的。

代理模式的用途很普遍,这里咱们看一个缓存代理的例子。

首先建立一个 Proxy 的包装函数,该函数接受须要建立代理的目标函数为第一个参数,以缓存的初值为第二个参数:

const createCacheProxy = (fn, cache = new Map()) => {
  return new Proxy(fn, {
    apply(target, context, args) {
      const argsProp = args.join(' ');
      if (cache.has(argsProp)) {
        console.log('Using old data...');
        return cache.get(argsProp);
      }
      const result = fn(...args);
      cache.set(argsProp, result);
      return result;
    }
  });
};
复制代码

而后咱们使用乘法函数 mult 去建立代理并调用:

const mult = (...args) => args.reduce((a, b) => a * b);

const multProxy = createCacheProxy(mult);

multProxy(2, 3, 4);  // 24
multProxy(2, 3, 4);  // 24, 输出 Using old data
复制代码

也可使用其余的函数:

const squareAddtion = (...args) => args.reduce((a, b) => a + b ** 2, 0);

const squareAddtionProxy = createCacheProxy(squareAddtion);

squareAddtionProxy(2, 3, 4);  // 29
squareAddtionProxy(2, 3, 4);  // 29, 输出 Using old data
复制代码

对于上面这个例子,有三点须要注意:

  • 对于检测是否存在旧值的过程较为粗暴,实际应用中应考虑是否应该使用更为复杂精确的判断方法,须要结合实际进行权衡;
  • createCacheProxy 中的 console.log 违背了前文所说的“代理和本体接口一致性”原则,只是为了开发环境更加方便性的调试,生产环境中必须去掉;
  • multProxysquareAdditionProxy 是为了演示使用方法而在这里使用了相对简单的算法和小数据量,但在实际应用中数据量越大、 fn 的计算过程越复杂,优化效果越好,不然,优化效果不只有可能不明显反而会形成性能降低

代理模式的实际应用

这一节结合几个具体的例子来加深对代理模式的理解。

函数节流

若是想要控制函数调用的频率,可使用代理进行控制:

须要实现的基本功能:

const handler = () => console.log('Do something...');
document.addEventListener('click', handler);
复制代码

接下来使用 Proxy 进行节流。

首先使用构造建立代理函数:

const createThrottleProxy = (fn, rate) => {
  let lastClick = Date.now() - rate;
  return new Proxy(fn, {
    apply(target, context, args) {
      if (Date.now() - lastClick >= rate) {
        fn(args);
        lastClick = Date.now();
      }
    }
  });
};
复制代码

而后只须要将原有的事件处理函数进行一曾包装便可:

const handler = () => console.log('Do something...');
const handlerProxy = createThrottleProxy(handler, 1000);
document.addEventListener('click', handlerProxy);
复制代码

在生产环境中已有多种工具库实现该功能,不须要咱们本身编写

图片懒加载

某些时候须要延迟加载图片,尤为要考虑网络环境恶劣以及比较重视流量的状况。这个时候可使用一个虚拟代理进行延迟加载。

首先是咱们最原始的代码:

const img = new Image();
img.src = '/some/big/size/image.jpg';
document.body.appendChild(img);
复制代码

为了实现懒加载,建立虚拟图片节点 virtualImg 并构造建立代理函数:

const createImgProxy = (img, loadingImg, realImg) => {
  let hasLoaded = false;
  const virtualImg = new Image();
  virtualImg.src = realImg;
  virtualImg.onload = () => {
    Reflect.set(img, 'src', realImg);
    hasLoaded = true;
  }
  return new Proxy(img, {
    get(obj, prop) {
      if (prop === 'src' && !hasLoaded) {
        return loadingImg;
      }
      return obj[prop];
    }
  });
};
复制代码

最后是将原始的图片节点替换为代理图片进行调用:

const img = new Image();
const imgProxy = createImgProxy(img, '/loading.gif', '/some/big/size/img.jpg');
document.body.appendChild(imgProxy);
复制代码

异步队列

这个需求是很常见的:前一个异步操做结束后再进行下一个异步操做。这部分我使用 Promise 进行实现。

首先构造一个最为简单的异步操做 asyncFunc

const callback = () => console.log('Do something...');

const asyncFunc = (cb) => {
  setTimeout(cb, 1000);
}

asyncFunc(callback);
asyncFunc(callback);
asyncFunc(callback);
复制代码

能够看到控制台的输出是 1s 以后,几乎是同时输出三个结果:

// .. 1s later ..
Do something...
Do something...
Do something...
复制代码

接下来咱们使用 Promise 实现异步队列:

const createAsyncQueueProxy = (asyncFunc) => {
  let promise = null;
  return new Proxy(asyncFunc, {
    apply(target, context, [cb, ...args]) {
      promise = Promise
        .resolve(promise)
        .then(() => new Promise(resolve => {
          Reflect.apply(asyncFunc, this, [() => {
            cb();
            resolve();
          }, ...args]);
        }));
    }
  });
};
复制代码

上面这段代码经过 Promise 实现了异步函数队列,建议在理解了 Promise 以后再理解阅读上面这段代码。

上面这段代码测试经过,有两点须要注意:

  • promise 的值并不能肯定是否为 Promise ,须要使用 Promise.resolve 方法以后才能使用 then 方法
  • Reflect.apply 方法中的第三个参数是数组,形同与 Function.prototype.apply 的第二个参数

而后使用代理进行替换并调用:

const timeoutProxy = createAsyncQueueProxy(asynFunc);

timeoutProxy(callback);
timeoutProxy(callback);
timeoutProxy(callback);
复制代码

能够看到控制台的输出已经像咱们指望的那样: 前一个异步操做执行完毕以后才会进行下一个异步操做。

// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
复制代码

除了上面这种使用代理的方式实现异步队列外,在个人另外一篇博客进阶 Javascript 生成器中,还使用了另一种方式。


结语

本文首先介绍了 ES2015 中关于 Proxy 的基本用法,接着讨论了代理模式的使用特色,而后结合实际列举了几种常见的使用场景。最后列举一些比较有价值的参考资料供感兴趣的开发者继续阅读。


参考资料