代理模式的定义:其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
简单来说代理模式就是访问一个对象的时候,我们不直接去访问,而是访问一个代理对象(中间人),这个代理对象再去对访问实际的对象,在这个代理对象中我们可以做一些事情,如:拓展功能、控制访问、提高性能等。
虚拟代理
图片预加载
在开发中我们常常会用到图片预加载的功能,当图片没有加载完成的时候显示一张 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); // 6mult(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 来实现:
// usePageimport { 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;
// usePageCacheProxyimport { 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 设计模式与开发实践。
