专栏首页一Li小麦JavaScript设计模式之代理模式

JavaScript设计模式之代理模式

程序设计从来就不是孤立于现实工作和生活的。生活中可以找到很多使用代理模式的场景。

当你想买商业保险的时候,却不得不亲自去了解不同公司的方案和限制。在自己百忙之中分神和不同的销售博弈。有了职业的保险经纪人,你只要向他/她讲述你的需求,经纪人就用自己的关系量身定做了一套方案给你。买哪家,怎么买,等等。保险经纪代理的模式,很好地体现了程序设计关注点分离的思想。

代理作为一种工作模式,代理也是非常重要。多人协作开发中,开发不应当直面具体的测试人员。而应该有项目经理之类的人梳理bug需求,根据项目计划区分优先级。然后程序员面向梳理后的bug保质保量完成目标,这样程序员就只顾修bug,而测试无需拿着bug去追着程序,出了问题先要排查前后端,然后还要排查环境问题,迟迟几天没有结果——那本不是测试该干的活,况且毫无意义。

代理场景

老电影《大鼻子情圣》有这样一个场景:A暗恋C,但他俩并不认识。刚好两人有一个共有的朋友B,于是A请求B帮忙送一束花给C。

用代码来表示就是:

const a={
  sendFlower(target){
    const flower=new Flower();
    target.receiveFlower(flower);
  },
  price:1000
}

const b={
  receiveFlower(flower){
    c.receiveFlower(flower);
  }
}

const c={
  receiveFlower(flower){
    console.log('收到fa')
  }
}

a.sendFlower(b)

b是个靠谱的代理,所以a送花给b等同于送花给c。

然而这种代码还是没什么用,接下来尝试让场景变得复杂些。假如B是个情商高,她会从送花的价格判断这个人有木有钱,穷逼就不要打扰了。另一方面,B还会挑C心情好的时候送花,因此需要监听C心情的变化,假设C在3秒之后心情会变好:

const b={
  receiveFlower(flower){
    c.listenGoodMood(()=>{
       if(a.price>999){
          c.receiveFlower(flower);
       }
      return false;
    })
  }
}

const c={
  receiveFlower(flower){
    console.log('收到fa')
  },
  listenGoodMood(fn){
    setTimeout(()=>{
        fn();
    },3000);
  }
}

从此可以看到代理的作用,B可以帮C过滤掉一些不好的人选,比如没钱的。B充当了黑脸的模式,而C是为了保持完美形象,不希望直接拒绝任何人。这样的模式称为保护代理

另一方面,从现实过程来看,上述的代理还存在问题。花是有保质期的(new FLower的代价高昂),如果C心情好的时候,再买花,就是最合适了。——这就是虚拟代理。它总是把一些开销很大的事情,放到需要用到的时候才执行。

图片的预加载

图片异步加载是一个非常常用的技术。非常适合使用代理模式。其实现机制是:创建一个普通的本体对象,负责往页面添加img标签,并提供一个setSrc方法:

const createImg=(function(){
  const img=document.createElelment('img');
  document.body.appendChild(img);

  return setSrc:(src)=>{
    img.src=src
  }
})();

createImg=setSrc('xxx.jpg');

当网速很慢时,这张图可能会加载很长的时间,此时考虑给加载中的图片设置一张loading图。

const proxyImg=(function(){
  const img=new Image();
  img.onload=function(){
    createImg.setSrc(this.src);
  };
  return {
    setSrc:function(src){
      createImg.setSrc('loading.gif');
      img.src=src;
    }
  }
})();

proxyImg.setSrc('xxx.jpg');

通过引入代理,把预加载的事给做了。

代理模式的意义

代理模式也完全可以在一个方法里实现。但是需要注意的是这违反了单一职责原则.。

前文提过,单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

如果引入代理,当任意需求变动后,单一职责原则就会很有用。在图片异步加载的案例中,倘若哪天网速变快,那就不再需要代理模式了。只需要把方法替换为原生的createImg即可。完全无痛。

节流

web世界里,最大的开销不是CPU,而是网络请求。设想,我有一个按钮点击一次就发送一次请求,那么不排除有几十年单身的玩家,在一秒钟连续点击按钮10次。这意味着连续触发了10次请求。因此节流就很重要了。

// 节流
    const throttle=(fn,interval)=>{
        const lastTime=0;
        return function(...args){
            let now=new Date();
            if(now-lastTime>interval){
                fn.apply(this,args);
                lastTime=now;
            }
        }
    }

    const todo=()=>{
      // ...
    }

    btn.addEventListener('click',function(e){
      throttle(todo,2000)
    })

继续节省开销

原书作者写过一个miniConsole框架,用户也不一定每个页面都需要用,我们希望在使用的时候才开始加载它。于是定义为按下f2时。在代码里,如果遇到执行miniConsole的代码,会被通通接受放到缓存(cache)里。一旦真正的miniConsole开始调用,就会以命令队列的形式去真正执行它:

let miniConsole={
  log:function(){
    window.cache.push(function(){
      return miniConsole.log.apply(miniCOnsole,arguments)
    })
  }
}

const handler = function (ev) { 
    if (ev.keyCode === 113) { 
        var script = document.createElement('script'); 
        script.onload = function () { 
            for (let i = 0, fn; fn = window.cache[i++];) { 
                fn(); 
            } 
        }; 
        script.src = 'miniConsole.js'; 
        document.getElementsByTagName('head')[0].appendChild(script); 
    } 
};

document.body.addEventListener('keydown',handler);

缓存代理

我曾经问过来面试前端这样一个问题。

一个web日历应用,点击请求付费接口获取黄历信息。如果用户有事没事点击,势必造成很大的开销。这时有什么解决策略?

其实这就是缓存代理的问题。考虑中转服务器代理请求,如果没有请求过,则由中转服务器请求,并且放到本地缓存中,如果有这天的数据,就从缓存中发回。前端一样是有一个代理,缓存已经发送过的请求结果。

这是笔者工作中遇到的另外一个场景:一个表格,包含所有的资金累计,由于原生js的不准确性,又不能映入其它的资金计算框架。只能调用后端接口进行计算。而在表格操作后,要求经常刷新表格——而统计对此表格并没有任何关联,却反复计算,浪费了资源。

这个时候就很适合使用缓存代理。我们尝试稍微抽象这个工作问题,用一个对象来缓存计算结果,只要参数一样,就返回一致的结果。

/**
* 此处模仿计算量大的计算
*/ 
const calc=function(a,b){
    console.log('计算了')
    return a+b;
}

const proxyCalc = (function() {
    let cache = {};

    return function(){
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }else{
            return cache[args] = calc.apply(this, arguments);
        }
    }
})();

proxyCalc(1,2) // 3
proxyCalc(1,2) // 3

你会发现,两次都返回了一致的结果,但是计算只有一次。

现在把原来的场景带上,如果calc是个异步计算呢?操作差不多我们无法直接把计算结果放到代理对象的缓存中,而是要通过promise的方式。具体代码如下:

const calc=function(a,b){
    console.log('计算了');
      // 模拟一个耗时的异步请求
    setTimeout(()=>{
        return a+b;
    });
}

const proxyCalc = (function() {
    let cache = {};

    return async function(){
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }else{
            cache[args] = await calc.apply(this, arguments);
        }
        return cache[args];
    }
})();


proxyCalc(1,2)
setTimeout(()=>{
    proxyCalc(1,2)
})

高阶函数

也许此时已经有很多读者燥起来了。 proxyCalc适用性太低了。可用的一个代理,应该是处理一类相似的计算方法,而不是一个。因此需要改写 proxyCalc

那么就考虑创建代理工厂:

var createProxyFactory=function(fn){
  varcache={};
  return function(){
        var args=Array.prototype.join.call(arguments,',');
    if(args in cache){
      return cache[args];
    }
    return cache[args]=fn.apply(this,arguments);
  }
};

你就可以用 createProxyFactory处理任意的数字计算了。

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Js算法与数据结构拾萃(6):回溯

    对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束满足问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一...

    一粒小麦
  • 那些年初级前后端一起撕过的逼

    一个项目一开始总是出于还不错愿景,但做着做着,就越来越乱了。万丈高楼平地起,有些基础的问题解决好,后面改需求就不会那么痛苦了。

    一粒小麦
  • 前后端权限机制

    本项目使用vue全家桶,axios和cube-ui cube-ui文档地址:https://didi.github.io/cube-ui/#/zh-CN/doc...

    一粒小麦
  • hdu1021

    @坤的
  • Android实现类似QQ对话框的@他人的整体解决方案

    在我们公司的新版APP中社区板块有个在回复回帖中有个@他们的功能,基本需求和QQ群组对话框里@群或组里任何一个成员类似。而数据传输方面,选择了直接传输富文本格式...

    1025645
  • 【leetcode刷题】T94-亲密字符串

    给定两个由小写字母构成的字符串 A 和 B ,只要我们可以通过交换 A 中的两个字母得到与 B 相等的结果,就返回 true ;否则返回 false 。

    木又AI帮
  • [C语言]N阶勒让德公式

    雨落凋殇
  • 力扣79——单词搜索

    单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

    健程之道
  • 回溯:系列经典题目

    对于回溯算法,一开始接触感觉还是挺难的,随着刷到的题目的数量增多,慢慢也可以总结出来相应的套路出来。大家一起来看看下面的伪代码

    鹏-程-万-里
  • 【leetcode刷题】T208-平方数之和

    https://leetcode-cn.com/problems/sum-of-square-numbers

    木又AI帮

扫码关注云+社区

领取腾讯云代金券