代理模式的定义:其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
简单来说代理模式就是访问一个对象的时候,我们不直接去访问,而是访问一个代理对象(中间人),这个代理对象再去对访问实际的对象,在这个代理对象中我们可以做一些事情,如:拓展功能、控制访问、提高性能等。
虚拟代理
图片预加载
在开发中我们常常会用到图片预加载的功能,当图片没有加载完成的时候显示一张 loading 图片,当图片加载完毕后将 loading 替换成真实图片,这样当用户网络慢的时候就不会看到一片空白提高了用户体验,这个场景就非常适合使用虚拟代理。
class PreLoadImage {
static loadingImageSrc = '/assets/xxx.jpg';
constructor(imageNode) {
this.imageNode = imageNode;
}
setSrc(url) {
// 先指向 loading 图
this.imageNode.src = PreLoadImage.loadingImageSrc;
// 利用 Image onload 来监测是否加载完毕
const image = new Image();
image.onload = () => {
// 加载完毕后再赋值
this.imageNode.src = url;
};
image.src = url;
}
}
上面的代码开启来是没问题的,但实际上违反了单一职责原则,这个类它做了两件事,1. 图片加载、2. 设置图片地址,现在我们用代理模式来实现一下,PreLoadImage
类只做 DOM 层面的事情,也就是图片设置,然后再创建一个 ProxyLoadImage
类来实现图片的加载判断。
class PreLoadImage {
constructor(imageNode) {
this.imageNode = imageNode;
}
setSrc(url) {
this.imageNode.src = url;
}
}
// 代理类
class ProxyLoadImage {
static loadingImageSrc = '/assets/xxx.jpg';
constructor(preLoadImage) {
this.preLoadImage = preLoadImage;
}
setSrc(url) {
// 先指向 loading 图
preLoadImage.setSrc(ProxyLoadImage.loadingImageSrc);
// 利用 Image onload 来监测是否加载完毕
const image = new Image();
image.onload = () => {
preLoadImage.setSrc(url);
};
image.src = url;
}
}
防抖函数、节流函数
在实际的开发中,节流、防抖函数常常用于性能优化,它们就属于虚拟代理的实现,我们不直接发起网络请求,而是用节流/防抖函数来去发起,它们会控制网络数量来达到性能优化的目的。
缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的储存,如果下一次运算的参数和之前的一致,就直接使用缓存,而无需再次运算。
计算乘积
const mult = (...args) => {
let result = 1;
for (let i = 0, len = args.length; i < len; i++) {
result = result * args[i];
}
return result;
};
mult(2, 3); // 6
mult(2, 3); // 重新计算:6
上面 mult
函数中每次调用都会重新进行计算,即使我们传递的参数是一样的,目前这个函数比较简单还是无所谓的,但如果是一个复杂计算的话,重复计算就有点浪费资源了,现在我们加载缓存代理看看:
const proxyMult = (() => {
const cache = {};
return (...args) => {
const key = args.join(',');
if (cache[key]) return cache[key];
return cache[key] = mult(...args);
};
})();
通过 proxyMult
函数来实现缓存功能,mult
函数来实现计算功能,每个函数都专注于自己的职责,当我们调用 proxyMult
传递相同的参数时,就直接返回函数的数据,而非重新计算了。
缓存代理用于网络请求
在开发中常常会遇到分页的需求,我们可以利用缓存代理来将数据缓存起来,这样下次再请求同一页的数据时便可以直接使用直接缓存的数据,而不用发起网络请求了。
这里使用 Vue3 的 Composition API 来实现:
// usePage
import { ref, toRefs, unref, reactive, watchPostEffect, type Ref } from 'vue';
export type PageRequest<T> = (
current: number,
size: number,
params?: Record<string, string>
) => Promise<{
total: number;
data: T[];
}>;
export type Option = {
isRequest?: Ref<boolean> | boolean;
};
function usePage<T>(request: PageRequest<T>, option?: Option) {
const data = ref([]) as Ref<T[]>;
const exception = ref();
const page = reactive({
current: 1,
size: 10,
total: 0,
});
const loading = ref(false);
const { isRequest } = option ?? {};
watchPostEffect(() => {
if (isRequest !== undefined && !unref(isRequest)) {
return;
}
loading.value = true;
const { current, size } = page;
request(current, size)
.then((res) => {
page.total = res.total;
data.value = res.data;
loading.value = false;
})
.catch((e) => {
exception.value = e;
});
});
return {
data,
loading,
...toRefs(page),
};
}
export default usePage;
// usePageCacheProxy
import { ref, computed, watch, watchEffect } from 'vue';
import usePage, { type PageRequest } from './usePage';
function usePageCacheProxy<T>(request: PageRequest<T>) {
const isRequest = ref(false);
const cache: Record<string, T[]> = {};
const result = usePage<T>(request, {
isRequest,
});
const cacheKey = computed(() => {
const { current, size } = result;
return `${current.value}:${size.value}`;
});
watch(result.data, (newData) => {
const key = cacheKey.value;
if (!cache[key]) {
// 缓存数据
cache[key] = newData;
}
});
watchEffect(() => {
const key = cacheKey.value;
if (cache[key]) {
// 有缓存, 不发起网络请求
isRequest.value = false;
return (result.data.value = cache[key]);
}
// 没有缓存, 发起网络请求
isRequest.value = true;
});
return result;
}
export default usePageCacheProxy;
整理与 JavaScript 设计模式与开发实践。