最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 设计模式|JavaScript实现代理模式(中篇)

    正文概述 掘金(sothx)   2021-06-29   293

    这是我参与更文挑战的第26天,活动详情查看:更文挑战

    文章引导

    代理模式篇幅较多,分为上、中、下三篇进行整理。

    JavaScript实现代理模式(上篇)

    内容梗概:主要介绍什么是代理模式,代理模式的简单实现

    链接:juejin.cn/post/697850…

    JavaScript实现代理模式(中篇)

    内容梗概:分析代理模式的各种用途

    链接:juejin.cn/post/697903…

    JavaScript实现代理模式(下篇)

    内容梗概:基于ES6 Proxy API实现更方便的代理模式

    链接:未发布

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

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

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

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

    但是在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。

    设计模式|JavaScript实现代理模式(中篇)

    这里把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…


    起源地下载网 » 设计模式|JavaScript实现代理模式(中篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元