做有态度的前端团队

网易FEG前端团队

JavaScript观察者模式的运用

观察者模式,是设计模式中的一种,具体含义和说明,这里就不写了,可以执行百度,这里主要讲解如何使用?如何改善现有的代码逻辑?如何解耦?等问题

一、最简单的例子

$("#test").on("click",function(e){
    alert("hi");
});

这是jQuery中的监听元素的点击事件,然后做出响应;其实这就是观察者模式的运用;监听某个事件,让其它模块来响应你,可能光这么说,感受不出它的好处,看下面的实例。

二、常规的模块间调用

用一个比较直接的例子说明,每个游戏中,都有生命值这个概念,当生命值为0时就结束游戏,假设有三个模块:Life-生命值管理模块,Animation-动画模块,Dead-死亡模块

//生命值管理模块
var Life = function(){

    //最大生命值
    var _max = 100;
    //当前生命值
    var _life = 100;
    //生命值变化接口
    function lifeChange(diff){
        //增加或者扣血
        _life += diff;

        //调用其它模块生命值变化接口
        Animation.lifeChange({life:_life,diff:diff});
        Dead.lifeChange({life:_life,diff:diff});
    }

    return {
        lifeChange : lifeChange
    }
}();

//动画模块,显示生命值变化动画
var Animation = function(){

    return {
        lifeChange : function(param){

            $("#life").animation({width:param.life + "px"});
        }
    };
}

//死亡模块,当生命值为空时结束游戏
var Dead = function(){

    return {
        lifeChange : function(param){
            if(param.life <= 0)return alert("game over");
        }
    }
}();

上面是简单的三个模块实现逻辑,大致思路为:

  1. 当生命值发生变化的时候,即有怪物砍了你一刀,调用了Life.lifeChange(-20)函数。
  2. 这个时候,生命值需要发生改变,_lift减掉20
  3. 然后需要让生命变化的过程有动画,调用了动画模块的Animation.lifeChange函数
  4. 之后要判断是否死亡了,调用了死亡模块的Dead.lifeChange函数

存在的问题

  1. 模块之间高度耦合,LifeAnimationDead之间存在互相依赖的关系
  2. 如果需要增加一个新模块,比如满血模块,会增加攻击力等,那你又需要在Life里增加一次函数调用,耦合的模块又增加多一个
  3. 不灵活,无法在当前模块,比如:Dead,得知lifeChange的调用在哪?以及时机?后期维护变得头疼

思考?要是能像jQuery的点击事件监听那样就好了~~

三、自定义事件监控和调用

自己实现类似jQuery的监听和调用的逻辑,使得模块的耦合降低

//简单的事件监听和执行
var Proxy = function(){

    //保存所有代理
    var _Proxy = {};

    return {
        //监听代理
        onProxy : function(namespace,action,func){

            //创建命名空间,避免事件名冲突
            if(!Proxy[namespace])Proxy[namespace] = {};
            if(!Proxy[namespace][action]) Proxy[namespace][action] = [];

            //添加代理到队列中
            Proxy[namespace][action].push(func);
        },
        //执行代理
        execProxy : function(namespace,action,param){

            if(!Proxy[namespace])return false;
            if(!Proxy[namespace][action])return false;

            //遍历代理队列,执行函数
            for(var i=0;i<Proxy[namespace][action].length;i++){

                Proxy[namespace][action][i](param);
            }
        }
    };
}();

实现思路

  1. 需要在原本调用lifeChange的地方,执行代理,即示例中的Life.lifeChange函数中
  2. 需要在原本提供接口给外部调用的模块,增加监听,即示例中的Dead.lifeChangeAnimation.lifeChange
  3. 修改如下
//生命值管理模块
var Life = function(){

    //最大生命值
    var _max = 100;
    //当前生命值
    var _life = 100;
    //生命值变化接口
    function lifeChange(diff){
        //增加或者扣血
        _life += diff;
        //执行生命值变量变化的事件代理
        Proxy.execProxy("Life","lifeChange",{life:_life,diff:diff});
    }

    return {
        lifeChange : lifeChange
    }
}();

//动画模块,显示生命值变化动画
var Animation = function(){

    function init(){

        //监听生命值变化
        Proxy.onProxy("Life","lifeChange",function(param){

            $("#life").animation({width:param.life + "px"});
        });
    }
    init();
}

//死亡模块,当生命值为空时结束游戏
var Dead = function(){

    function init(){

        //监听生命值变化
        Proxy.onProxy("Life","lifeChange",function(param){
            
            if(param.life <= 0)return alert("game over");
        });
    }
    init();
}();

实现逻辑

  1. Life模块中,不再需要知道具体需要调用哪个模块的哪个函数了,只需要统一执行一次代理就可以了
  2. AnimationDead模块,不再需要提供接口给外部了,只需要监听lifeChange事件,就可以执行对应的逻辑
  3. 强制解耦三个模块之间的联系
  4. 即使增加多几个模块,也不需要修改Life模块,各自模块监听需要的事件即可

小结

设计模式,在很多时候能够帮助我们更好的管理和设计代码、模块等,善加利用,能够事半功倍

手机阅读请扫描下方二维码: