前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript设计模式之代理模式

JavaScript设计模式之代理模式

作者头像
一粒小麦
发布2019-10-30 14:43:25
3230
发布2019-10-30 14:43:25
举报
文章被收录于专栏:一Li小麦一Li小麦
程序设计从来就不是孤立于现实工作和生活的。生活中可以找到很多使用代理模式的场景。

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

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

代理场景

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

用代码来表示就是:

代码语言:javascript
复制
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秒之后心情会变好:

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
const createImg=(function(){
  const img=document.createElelment('img');
  document.body.appendChild(img);

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

createImg=setSrc('xxx.jpg');

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

代码语言:javascript
复制
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次请求。因此节流就很重要了。

代码语言:javascript
复制
// 节流
    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开始调用,就会以命令队列的形式去真正执行它:

代码语言:javascript
复制
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的不准确性,又不能映入其它的资金计算框架。只能调用后端接口进行计算。而在表格操作后,要求经常刷新表格——而统计对此表格并没有任何关联,却反复计算,浪费了资源。

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

代码语言:javascript
复制
/**
* 此处模仿计算量大的计算
*/ 
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的方式。具体代码如下:

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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处理任意的数字计算了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代理场景
  • 图片的预加载
  • 代理模式的意义
  • 节流
  • 继续节省开销
  • 缓存代理
  • 高阶函数
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档