阅读 2580

使用 Javascript 原生的 Proxy 优化应用

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


Proxy 的基本使用

开始学习 Proxy 的使用之前,建议首先对 Reflect 有一定的了解,如果很陌生的话,建议先花 1 分钟浏览相关知识。

好了,现在假设已经具备了一定的 Reflect 知识,就开始掌握 Proxy 吧。

基本语法

Proxy 相关的方法一共就两个:

  • 构造方法 本文着重讨论
  • Proxy.revocable() 创建一个可撤销的 Proxy 对象,其余与构造函数类似,理解了 Proxy 的构造方法后,该方法与构造方法使用非常类似,本文不再涉及

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

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

参数

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

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

返回

  • p Proxy 对象

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

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

代理一个对象字面量:

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 的基本用法,接着讨论了代理模式的使用特点,然后结合实际列举了几种常见的使用场景。最后列举一些比较有价值的参考资料供感兴趣的开发者继续阅读。


参考资料

评论