javascript-设计模式-代理模式

内容大部分来自 《javascript 设计模式与开发实践》一书,适当的结合自己工作实践,做出整理和笔记
系列: javascript 设计模式

代理模式

  • 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
  • 代理和本体的一致性要求,代理和本体需要对外提供一致的方法,代理请求接手的过程对用户来说是透明的,用户可以安心的访问代理,在任何地方都可以使用本体来代替代理。
  • 代理模式体现在对本体对象的额外附带或者加强的能力上,让本体对象只处理原本的任务,符合单一职责原则。

常见代理模式

  1. 虚拟代理:把开销大的操作,放到真真需要的时候才会去执行
  2. 保护代理:控制不同权限的对象对目标对象的访问,Javascript并不容易实现保护代理,本章内容也主要讨论的是虚拟代理。例如:
    判断需要给服务器发送消息,可以判断通过保护代理知道对方在线的时候再发消息给对方,这样在请求代理的时候可以直接返回对方不在线而拒绝掉。

虚拟代理 - 图片懒加载

在日常开发中需要对一个图片设置src,由于图片加载需要等待时间所以会出现图片空白的场景,下面来用代理模式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 额外添加一个方法获取一个一像素的色点
const getLoadingImg = (fillStyle = 'red') => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.fillStyle = fillStyle;
ctx.fillRect(0, 0, 1, 1);
return canvas.toDataURL();
};

// 首先创建本体对象,做图片加载的操作
const myImage = (function myImage() {
const imgNode = document.createElement('img');
imgNode.style.minHeight = '100px';
imgNode.style.minWidth = '100px';

document.body.appendChild(imgNode);
return function fn(src) {
imgNode.src = src;
};
}());

const proxyImage = (function() {
const img = new Image();
img.onload = function() {
console.log('img load:' + this.src);
myImage(this.src);
};
return function fn(src) {
myImage(getLoadingImg());
img.src = src;
};
}());

// 执行图片加载
proxyImage('https://uidesign.gbtcdn.com/GB/image/2019/20191115_13916/1190x420.jpg?imbypass=true');

如上:
myImage 方法只处理和显示图片相关的逻辑,proxyImage 为图片显示加上了懒加载的能力,这样就符合 单一职责原则 某一天你不需要这里使用懒加载的功能也可以很方便的修改。

虚拟代理 - 惰性应用

惰性应用可以通过代理方法实现,在真真需要的时候才去执行对本地的调用。在一些需要较大的操作时,可以延迟或者按需对本体的执行,对于性能提升很有帮助。
不做详细演示

缓存代理

缓存带了可以为一些开销大的操作提升暂时存储的能力,如果传递进来的参数保持一致可以支持返回。
例如:我需要设计一个方法返回斐波那契函数前N个的数的数字是多少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 先创建一个方法用于获取前N个的值分别是多少,0开始
function fibonacciArray(n) {
const res = [];

let i = 0;
while (i <= n) {
if (i <= 1) {
res.push(1);
} else {
res.push(res[i - 1] + res[i - 2])
}
i += 1;
}

return res;
}
console.time('fibonacciArray');
console.log(fibonacciArray(40));
console.timeEnd('fibonacciArray');

下面换一种思维通过递归调用获取需要第N个的值,如下方法会存在较大的性能问题

1
2
3
4
5
6
7
8
9
10
11
12
// 例子来源于前同事的
function fibonacci(n) {
if (n <= 1) {
return 1;
} else {
return fibonacci(n -2) + fibonacci(n -1);
}
}

console.time('fibonacci');
console.log(fibonacci(40));
console.timeEnd('fibonacci');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这是一个失败了的例子并没用使用代理的痕迹,不修改源码的情况下递归调用会导致运行到本体函数
const proxyFibonacci = (() => {
const cache = {};
return (n) => {
if (n in cache) {
return cache[n];
} else if (n <= 1) {
return 1;
} else {
cache[n - 2] = proxyFibonacci(n - 2);
cache[n - 1] = proxyFibonacci(n - 1);
cache[n] = cache[n - 2] + cache[n - 1];
return cache[n];
}
}
})();
console.time('proxyFibonacci');
console.log(proxyFibonacci(40));
console.timeEnd('proxyFibonacci');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 本体操作,假设很耗时的操作
function computedX(...args) {
return args.reduce((preVal, curVal) => {
// preVal *= curVal;
preVal[curVal] = curVal;
return preVal;
}, {});
}

const computedX1 = computedX(1,2,3,4,5,6);
const computedX2 = computedX(1,2,3,4,5,6);

console.log('computedX1', computedX1);
console.log('computedX2', computedX2);
console.log('computedX1 === computedX2', computedX1 === computedX2);

const proxyComputedX = (() => {
const cache = {};
return (...args) => {
const cacheKey = args.join(',');
if (!(cacheKey in cache)) {
cache[cacheKey] = computedX(...args);
}
return cache[cacheKey];
};
})();

const proxyComputedX1 = proxyComputedX(1,2,3,4,5,6);
const proxyComputedX2 = proxyComputedX(1,2,3,4,5,6);

console.log('proxyComputedX1', proxyComputedX1);
console.log('proxyComputedX2', proxyComputedX2);
console.log('proxyComputedX1 === proxyComputedX2 :', proxyComputedX1 === proxyComputedX2);

这样缓存的能力是由代理提供的,符合单一职责原则

高阶函数动态创建缓存代理

创建代理函数工厂,缓存函数的执行结果,类似场景代理方法都可以通过该代理工厂创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 本体操作,假设很耗时的操作
function computedX(...args) {
return args.reduce((preVal, curVal) => {
// preVal *= curVal;
preVal[curVal] = curVal;
return preVal;
}, {});
}

// 代理函数工厂方法
const createProxyFactory = function createProxyFactory(fn) {
const cache = {};
return function proxyFn(...args) {
const argsStr = args.join(',');

if (!(argsStr in cache)) {
cache[argsStr] = fn.apply(this, args);
}

return cache[argsStr];
};
};

const proxyFn = createProxyFactory(computedX);

const proxyFn1 = proxyFn(1,2,3,4,5,6);
const proxyFn2 = proxyFn(1,2,3,4,5,6);

console.log('proxyFn1', proxyFn1);
console.log('proxyFn2', proxyFn2);
console.log('proxyFn1 === proxyFn2 :', proxyFn1 === proxyFn2);

小结

代理模式包括许多小分类,在开发中最常用的是虚拟代理和缓存代理。
虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。
当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。