javascript设计模式六:发布-订阅模式(观察者模式)

发布-订阅模式也叫观察者模式,是js开发中应用广泛的一种模式。下面将列举一个通用发布订阅模式的示例,应用到闭包、this、apply/call、自执行函数等概念,起码达到熟悉的程度,才能说把发布-订阅模式真正吃透并能灵活运用到实际场景中去。

常见的发布订阅模式应用场景有:登录后head/nav等模块异步获取登录成功返回的数据;页面无刷新点击事件进行数据自增…

var ObserverEvent = (function(){
    var cacheList = {},     //缓存列表,存放已订阅的事件回调
        listen,             //订阅命名事件和对应事件回调
        trigger,            //触发命名事件,必传第一个参数为事件的命名,其后参数为选传,数量不限,用于作为事件回调的实参传入
        remove;             //取消命名事件订阅,并清除该命名事件对应的事件回调
    
    listen = function(key, fn){ 
        if(!cacheList[key]){    //如果还没有订阅过此命名事件,就给该命名事件创建一个数组型的缓存列表
            cacheList[key]=[]   
        }

        cacheList[key].push(fn)     //将对应的事件回调传入该命名事件的缓存列表中
    };

    trigger = function(){       
        var key = Array.prototype.shift.call(arguments),     //取出事件命名
            fns = cacheList[key];       //取出该命名事件对应的事件回调缓存列表

        if(!fns || fns.length==0){      //如果没有订阅该命名事件或对应的事件回调缓存列表为空数组,则直接返回false
            return false
        }

        //forEach参数用es5函数写法时,注意要将外层arguments对象预先保存引用再传进行该参数中
        //var restParameters = arguments  //将截取首位元素后arguments赋值给新的变量引用,供传递如下
        // fns.forEach(function(fn, i){
        //     fn.apply(this, restParameters)
        // })

        //forEach用es6函数写法时,由于es6的箭头函数不暴露arguments对象,所以可以在箭头函数中使用arguments,因为它指向的是其外层函数中的arguments对象。
        // fns.forEach((fn)=>{     
        //     fn.apply(this, arguments)
        // })

        //遍历该命名事件对应的事件回调缓存列表数组,对数组中的每个事件回调传入处理后的实参列表,然后执行
        for(var i=0; i<fns.length; i++){
            //arguments是发布消息时传入的参数,由于arguments已经由shift()方法截取出首位数值并保留剩余数值,所以传入的参数为剩余的数值
            //这里知识点: shitf()方法删除数组的第一个元素,并返回第一个元素,且该方法直接操作原数组,也就是原数组已被修改
            fns[i].apply(this, arguments)
        }
    };

    remove = function(key, fn){
        var fns = cacheList[key]

        if(!fns || fns.length==0){      //如果key对应的消息没有被订阅,则直接返回
            return false
        }

        if(!fn){    //如果没有显式传入具体的事件回调函数,则清除该命名事件对应的所有事件回调缓存
            fns.length=0
        }else {
            for(var l=fns.length-1; l>=0; l--){
                var _fn = fns[l]
                if(_fn == fn){
                    fns.splice(l, 1)
                }
            }
        }
    };

    return {
        cacheList,
        listen,
        trigger,
        remove
    }
})()


//定义发布对象安装函数,这个函数可以给所有的对象动态安装发布-订阅功能
let installEvent = function(obj){
    for(var i in ObserverEvent){
        obj[i] = ObserverEvent[i]
    }
}

//创建需要发布功能的某个对象
var loginModel = {};
installEvent(loginModel)    //为该对象装载发布订阅功能



loginModel.listen('loginSucc', function(price){
    console.log(price);
})

setTimeout(function(){
    loginModel.trigger('loginSucc', 2000)
}, 3000)

//打印结果
//3秒后打印:
//2000

发布订阅模式可以为模块间通信提供连接桥梁,沿用上例的全局发布订阅模块,示例如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>模块间通信</title>
</head>
<body>
    <div id="btn1">点我</div>
    <div id="show">0</div>
</body>
<script>
    var a = (function(){
        var count = 0
        document.getElementById('btn1').onclick = function(){
            ObserverEvent.trigger('add', ++count)
            // Event.remove('add')
        }
    })()

    var b = (function(){
        ObserverEvent.listen('add', function(count){
            document.getElementById('show').innerHTML = count
        })
    })()

    //模块a和模块b之间通信,模块b监听add回调函数,模块a通过点击事件触发add回调函数
</script>
</html>

再来个示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>发布-订阅模式</title>
</head>
<body>
    <button type="button" id="btn">点击发射</button>
    <button type="button" id="btn2">删除一个发射</button>
</body>
<script>
    let EventEmitter = function () {
        let cacheList = {},     // 缓存列表,存放已订阅的事件回调
            listen,             // 订阅命名事件和对应的事件回调
            emit,               // 触发命名事件,必传第一个参数为事件的命名,其余参数为选传,数量不限,用于作为事件回调的实参传入
            remove;             // 取消命名事件的订阅,并清除该命名事件对应的事件回调
        
        listen = function ( key, fn ) {
            // 如果还没有订阅过此命名事件,就给该命名事件创建一个数组缓存列表,用于存放对应的事件回调
            if( !cacheList[key] ){      
                cacheList[key] = [];
            }
            // 将对应的事件回调传入该命名事件的缓存列表中
            cacheList[key].push( fn );
        };

        emit = function () {
            let key = Array.prototype.shift.call( arguments ),      // 获取将要触发的事件命名
                fns = cacheList[key];                               // 获取缓存列表中该命名事件订阅的事件回调函数
            
            // 如果没有预存事件回调或该命名事件对应的事件回调缓存列表为空数组,直接返回false
            if( !fns || fns.listen === 0 ){
                return false;
            }

            // 遍历并执行触发的命名事件对应的事件回调数组中每个事件回调函数
            for( let i=0; i<fns.length; i++ ){
                // tips:arguments为触发命名事件时传入的参数类数组,此时arguments已被取出索引为0处的事件命名,剩余元素就是要传入事件回调中的所有参数
                fns[i].apply( this, arguments );
            }
        };

        remove = function ( key, fn ) {
            // 获取将要被删除的事件命名的事件回调缓存列表
            let fns = cacheList[key];

            // 如果没有预存事件回调或该命名事件对应的事件回调缓存列表为空数组,直接返回false
            if( !fns || fns.listen === 0 ){
                return false;
            }

            if( !fn ){
                // 如果没有传入具体事件回调,则清除该事件命名已缓存的所有事件回调函数
                fns.length = 0;
            }else {
                // 遍历事件命名对应的事件回调缓存列表,如传入要删除的事件回调函数与缓存列表数组中的某项匹配,就删除该项
                for( let l=fns.length-1; l>=0; l-- ){
                    let _fn = fns[l];
                    if( _fn === fn ){
                        fns.splice( l, 1 );
                    }
                }
            }
        }

        return {
            cacheList,
            listen,
            emit, 
            remove
        }
    }

    let emitter = new EventEmitter();

    function a( data ) {
        console.log( data );
    }

    function b( data ) {
        console.log( data + "aa" );
    }

    emitter.listen( "go", a )

    emitter.listen( "go", b )

    document.getElementById( "btn" ).onclick = function ( e ) {
        emitter.emit( "go", e.target.innerHTML )
    }

    document.getElementById( "btn2" ).onclick = function ( e ) {
        emitter.remove( "go", a )
    }
</script>
</html>

其实这个发布订阅模式,在nodejs中就有完美实现,即events.EventEmitter,nodejs大部分API都继承自这个模块:

const events = require( "events" );
const emitter = new events.EventEmitter();

emitter.on( "go", function ( data ) {
    console.log( data );
} )

setTimeout( function () {
    emitter.emit( "go", "人间大炮,发射!" )
}, 2000 )

最后插个闭包的相关吧。

怎么理解面向对象中的对象呢?对象是过程和数据的结合,对象以方法的形式包含了过程,在方法中可以用this访问到所处对象环境中的数据,以供方法中的过程使用。

怎么理解闭包呢?闭包在过程中以上下文环境的形式包含了数据,即闭包始终保持对上下文环境中数据的引用。

发现共同点了么?

对象过程与闭包过程都能始终保持对所处上下文环境数据的引用。

show代码:

//闭包
var count = function(){
    var val = 0;
    return function(){
        console.log(val)
        val++
    }
}
var calcute = count()
calcute()   //0
calcute()   //1
calcute()   //2

//面向对象
var count = {
    val: 0,
    add: function(){
        console.log(this.val)
        this.val++
    }
}
count.add() //0
count.add() //1
count.add() //2

//或者用构造函数写面向对象
var Count = function(val){
    this.val = val
}
Count.prototype.add = function(){
    console.log(this.val)
    this.val++
}
var c1 = new Count(0)
c1.add()    //0
c1.add()    //1 
c1.add()    //2

喜欢本文请扫下方二维码,关注微信公众号: 前端小二,查看更多我写的文章哦,多谢支持。

javascript设计模式六:发布-订阅模式(观察者模式)

匿名

发表评论

匿名网友