代理模式是一种结构化设计模式(小灰的文章认为也可以算作是行为型设计模式),代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,这就是代理模式的定义。通俗的讲,代理模式的核心是在被调用方和调用方之间增加一个中介者的角色,也就是代理。

proxy-1.png

图源自小灰的文章"什么是代理模式?"

现实生活中,比如我们有租房需求,可能就需要经过房屋中介,让我们认识能够找到合适的房东。

在求职高薪岗位的时候,我们也需要找到猎头,给我们推荐合适的公司。

代理模式在现实生活中无处不在……

当然,你觉得代理模式可能会让简单的事情变复杂,但中介者的角色实际上会给你减少很多麻烦和成本,在代码中代理模式可以避免对业务类的侵入,把日志、事务之类和业务无关的辅助功能单独拎出来。

代理模式有以下两个优点:

中介隔离作用:

在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

符合代码设计的开放-封闭原则:

代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开放-封闭原则

代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。

真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

代理模式虽然和装饰者模式很相似,但是装饰器模式会对装饰对象增加功能,而代理模式并不会对源对象有改变,从外层去操作了这个对象,对象本身是不会有其他的改变。

代理模式的缺点主要是增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式。

实现代理模式

这里用《JavaScript设计模式与开发实践》一书中的例子。

小明想要追一个小姐姐,想给小姐姐送一束花表白。在不使用代理模式和使用代理模式的简单例子。

不使用代理模式

ES5

// 鲜花类
var Flower = function(){};
// 小明
var xiaoming = {
    sendFlower: function( target ){
        var flower = new Flower();
        target.receiveFlower( flower );
    }
};
// 小姐姐
var cuteGirl = {
    receiveFlower: function( flower ){
        console.log( '收到花 ' + flower );
    }
};
// 小明将鲜花直接交给小姐姐
xiaoming.sendFlower( cuteGirl );

ES6

// 鲜花类
class Flower {
    
}
// 小明
let xiaoming = {
    sendFlower(target){
        let flower = new Flower();
        target.receiveFlower( flower );
    }
}
// 小姐姐
let cuteGirl = {
    receiveFlower(flower){
        console.log( '收到花 ' + flower );
    }
}
// 小明将鲜花直接交给小姐姐
xiaoming.sendFlower( cuteGirl );

使用代理模式

以上就是不使用代理模式的例子,小明在不了解小姐姐喜好的情况下,贸然直接表白被拒绝的概率非常大,但是小明的好朋友恰好认识小姐姐的舍友,舍友会在小姐姐心情好的时候,帮小明把鲜花转交给小姐姐。

ES5

// 鲜花类
var Flower = function(){};
// 小明
var xiaoming = {
    sendFlower: function( target){
        var flower = new Flower();
        target.receiveFlower( flower );
    }
};
// 好朋友
var goodFriend = {
    receiveFlower: function( flower ){
        cuteGirl.listenGoodMood(function(){    // 监听A的好心情
            cuteGirl.receiveFlower( flower );
        });
    }
};
// 小姐姐
var cuteGirl = {
    receiveFlower: function( flower ){
        console.log( '收到花 ' + flower );
    },
    listenGoodMood: function( fn ){
        setTimeout(function(){    // 假设10秒之后A的心情变好
            fn();
        }, 10000 );
    }
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( goodFriend );

ES6

// 鲜花类
class Flower {
    
}
// 小明
let xiaoming = {
    sendFlower(target){
        let flower = new Flower();
        target.receiveFlower( flower );
    }
};
// 好朋友
let goodFriend = {
    receiveFlower(flower){
        cuteGirl.listenGoodMood(() => {    // 监听A的好心情
            cuteGirl.receiveFlower( flower );
        });
    }
};
// 小姐姐
let cuteGirl = {
    receiveFlower( flower ){
        console.log( '收到花 ' + flower );
    },
    listenGoodMood( fn ){
        setTimeout(() => {    // 假设10秒之后A的心情变好
            fn();
        }, 10000 );
    }
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( goodFriend );

使用代理模式中的保护代理

保护代理用于对象应该具有不同访问权限的场景,控制对原始对象的访问。

还是用上述书中的例子,因为小明和小姐姐的舍友是好朋友,她了解小明的为人,所以愿意为小明转送鲜花给小姐姐。

而如果把小明换成一个不相干的人,那么小姐姐的舍友不可能会答应这个奇怪的请求。

但是在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。所以我们对下面的例子进行简单的改造,给鲜花类增加来源,从而实现简单的保护代理。

// 鲜花类
class Flower {
    constructor(source) {
        this.source = source;
    }
}

// 小明
let xiaoming = {
    sendFlower(target){
        let flower = new Flower('xiaoming');
        target.receiveFlower( flower );
    }
};

// 路人
let passerby = {
    sendFlower(target){
        let flower = new Flower('passerby');
        target.receiveFlower( flower );
    }
};
// 小姐姐的闺蜜
let ladybro = {
    receiveFlower(flower){
        if (flower.source === 'xiaoming') {
            cuteGirl.listenGoodMood(() => {    // 监听A的好心情
              cuteGirl.receiveFlower( flower );
            });
        } else {
            throw new Error('小姐姐的闺蜜拒绝帮你送花!')
        }
    }
};
// 小姐姐
let cuteGirl = {
    receiveFlower( flower ){
        console.log( '收到花 ' + flower );
    },
    listenGoodMood( fn ){
        setTimeout(() => {    // 假设10秒之后A的心情变好
            fn();
        }, 10000 );
    }
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( ladybro );
// 路人将鲜花交给小姐姐的闺蜜,委托她在小姐姐心情好的时候将鲜花转交给小姐姐
passerby.sendFlower( ladybro );

使用代理模式中的虚拟代理

还是用上面书中的例子,鲜花的种类有很多种,每种鲜花的售价也不近相同,不同的鲜花也有不同的保质期。

小明为了夺得小姐姐的欢心,希望小姐姐的闺蜜在小姐姐心情好的时候,再去帮忙购买一束比较昂贵的鲜花转送给小姐姐,此时的操作就叫虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

// 小姐姐的闺蜜
let ladybro = {
    receiveFlower(flower){
        if (flower.source === 'xiaoming') {
            cuteGirl.listenGoodMood(() => {    // 监听A的好心情
              let flower = new Flower('xiaoming'); // 延迟创建flower 对象
              cuteGirl.receiveFlower( flower );
            });
        } else {
            throw new Error('小姐姐的闺蜜拒绝帮你送花!')
        }
    }
};

常见的虚拟代理实现

图片预加载

这里也是引用书中的例子,常见的开发需求之一,在图片未加载回来之前,希望有一个loading图进行占位,等loading图加载回来后再填充到img节点。

未使用代理模式

let MyImage = (function(){
    let imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    // 创建一个Image对象,用于加载需要设置的图片
    let img = new Image;

    img.onload = function(){
        // 监听到图片加载完成后,设置src为加载完成后的图片
        imgNode.src = img.src;
    };

    return {
        setSrc: function( src ){
            // 设置图片的时候,设置为默认的loading图
            imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif';
            // 把真正需要设置的图片传给Image对象的src属性
            img.src = src;
        }
    }
})();

MyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );

以上是未使用代理模式的写法,这也是常常容易写出来的代码情况,它在实现业务上并没有什么问题,但是MyImage对象除了负责给img节点设置src外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。

它同时还违反了开放—封闭原则,根据开放—封闭原则:

软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。

英文预加载loading的这个功能,是耦合进MyImage对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage对象里面改动代码。虽然MyImage改动代码只需要几行就可以解决问题,但是换做其他甚至拥有10万行代码级别的JavaScript项目,要修改它的源代码风险就很大了。

使用代理模式

// 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口
let myImage = (function(){
    let imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );

    return {
        //setSrc接口,外界调用这个接口,便可以给该img标签设置src属性
        setSrc: function( src ){
            imgNode.src = src;
        }
    }
})();
// 代理对象,负责图片预加载功能
let proxyImage = (function(){
    // 创建一个Image对象,用于加载需要设置的图片
    let img = new Image;
    img.onload = function(){
        // 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图片
        myImage.setSrc( this.src );
    }
    return {
        setSrc: function( src ){
            // 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加载
            myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' );
            img.src = src;
        }
    }
})();

proxyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );

在使用了代理模式后:

图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口;

代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功能;

同时,它也满足了开放—封闭原则的基本思想:

开放—封闭原则的基本思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为(这里的行为是图片预加载)。

合并HTTP请求

这里也是引用一个书中的例子,例如我们需要做一个文件同步的功能,在选中对应的文件时,需要被同步到自己的OneDrive。

proxy-2.png

这里把OneDrive中的同步文件夹替换成网页中的checkbox

<body>
    <input type="checkbox" id="1"></input>1
    <input type="checkbox" id="2"></input>2
    <input type="checkbox" id="3"></input>3
    <input type="checkbox" id="4"></input>4
    <input type="checkbox" id="5"></input>5
    <input type="checkbox" id="6"></input>6
    <input type="checkbox" id="7"></input>7
    <input type="checkbox" id="8"></input>8
    <input type="checkbox" id="9"></input>9
</body>
未使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
     console.log( '开始同步文件,id为: ' + id );
};
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
    // 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求
    c.onclick = function(){
        if ( this.checked === true ){
            synchronousFile( this.id );
        }
    }
};

在未使用代理模式时,每选中一次checkbox,就会触发一次同步文件请求,频繁的网络请求,会给服务器带来比较大的开销,此时我们可以在不改变synchronousFile函数职能的情况下,将它进行代理。

使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
    console.log( '开始同步文件,id为: ' + id );
};
// 同步文件请求的网络操作函数-代理函数
let proxySynchronousFile = (function(){
    let cache = [],    // 保存一段时间内需要同步的ID
        timer;    // 定时器

     return function( id ){
        cache.push( id );
        if ( timer ){    // 保证不会覆盖已经启动的定时器
            return;
        }

        timer = setTimeout(function(){
            synchronousFile( cache.join( ',' ) );    // 2秒后向本体发送需要同步的ID集合
            clearTimeout( timer );    // 清空定时器
            timer = null;
            cache.length = 0; // 清空ID集合
        }, 2000 );
    }
})();
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
    c.onclick = function(){
        // 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求的代理函数
        if ( this.checked === true ){
            proxySynchronousFile( this.id );
        }
    }
};

synchronousFile函数被代理后的函数我们起名为proxySynchronousFile,它增加了一个缓存数组,所有两秒内的checkbox选中,都会被添加到缓存数组check中,等待2秒之后才把这2秒之内需要同步的文件ID一次性全打包发给服务器(将多个id拼接成逗号分割的字符串),在实时性要求不是很高的系统,这能大大减少服务器的压力。

惰性加载中的应用

来自于书中的例子,假设有一个迷你控制台的项目——miniConsole.js,它有一个log函数,专门用于打印参数。

// miniConsole.js代码

let miniConsole = {
    log: function(){
        // 真正代码略
        console.log( Array.prototype.join.call( arguments ) );
    }
};

export default miniConsole

因为这个控制台项目,是只在控制台展示的时候才需要的,我们希望他在有必要的时候才开始加载它,比如按F2的时候,加载miniConsole.js,就可以使用代理模式,惰性加载miniConsole.js。

大致的步骤是:

  1. 在用户敲击F2的时候,才去动态引入miniConsole.js的script标签

  2. 在用户敲击F2之前执行过的log命令,都会被缓存到代理对象内部的cache缓存数组内

  3. 等动态引入miniConsole.js的操作完成后,再从中逐一取出并执行。

详细代码如下:

// proxyMiniConsole.js代码

// miniConsole的代理对象
let proxyMiniConsole = (function(){
    // 存储每次执行log时的回调函数
    let cache = [];
    let handler = function( ev ){
        // 如果用户按了F2唤出了控制台
        if ( ev.keyCode === 113 ){
            // 执行引入miniConsole.js的操作
            let script = document.createElement( 'script' );
            script.src = 'miniConsole.js';
            document.getElementsByTagName( 'head' )[0].appendChild( script );
            document.body.removeEventListener( 'keydown', handler );// 只加载一次miniConsole.js
            script.onload = function(){
                // 如果miniConsole.js的script标签引入并加载完成
                for ( var i = 0, fn; fn = cache[ i++ ]; ){
                    // 遍历所有缓存的回调函数并执行
                    fn();
                }
            };
        }
    };
    
    // 监听键盘按键敲击事件
    document.body.addEventListener( 'keydown', handler, false );

    return {
        // 返回代理后的方法
        log: function(){
            // 获取传入的所有参数
            let args = arguments;
            // 向缓存列表加入要打印的参数
                cache.push( function(){
                    return miniConsole.log.apply( miniConsole, args );
                });
          }
    }
})();

miniConsole.log( 11 );      // 开始打印log

使用代理模式中的缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

计算乘积

/**************** 计算乘积 *****************/
let mult = function(){
    let a = 1;
    for ( let i = 0, l = arguments.length; i < l; i++ ){
        a = a * arguments[i];
    }
    return a;
};

/**************** 计算加和 *****************/
let plus = function(){
    let a = 0;
    for ( let i = 0, l = arguments.length; i < l; i++ ){
        a = a + arguments[i];
    }
    return a;
};

/**************** 创建缓存代理的工厂 *****************/
let createProxyFactory = function( fn ){
    // 缓存计算后的结果
    let cache = {};
    return function(){
        // 通过字符串拼接所有传入的参数
        let args = Array.prototype.join.call( arguments, ',' );
        // 如果这个参数存在缓存内
        if ( args in cache ){
            // 则直接返回缓存的结果
            return cache[args];
        }
        // 否则再对这个值进行计算
        return  cache[args] = fn.apply( this, arguments );
    }
};

let proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( plus );

console.log ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
console.log ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24
console.log ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10
console.log ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10

参考资料

[CUG-GZ]前端知识进阶——代理模式

www.yuque.com/cuggz/feplu…

前端设计模式之代理模式

juejin.cn/post/684490…

漫画:什么是 “代理模式” ?

mp.weixin.qq.com/s/O8_A2Ms9M…

JavaScript设计模式与开发实践

www.ituring.com.cn/book/1632

从ES6重新认识JavaScript设计模式(五): 代理模式和Proxy

segmentfault.com/a/119000001…

使用 JavaScript 原生的 Proxy 优化应用

juejin.cn/post/684490…

说点什么吧...