发布订阅模式

发布订阅模式又叫观察者模式,他定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖于他的对象都将得到通知,在Js开发中我们一般用事件模式代替传统的发布订阅模式。

发布订阅模式实现的步骤

1.指定发布者

2.给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者

3.发布消息的时候发布者会遍历整个缓存列表,依次触发里面的回调函数

一个售楼处的例子,售楼处可以在发给订阅者的信息里加上房子的单价,面积等信息,订阅者接受到这个信息进行各自的处理

售楼处的例子
var salesOffices = {};
salesOffices.clientList = [];
salesOffices.listen = function(fn){
    // 发布者缓存回调函数
    salesOffices.clientList.push(fn);
}
salesOffices.trigger = function(){
    for(var i=0,fn;fn = this.clientList[i];i++){
        fn.apply(this,arguments)
    }
}

salesOffices.listen(function(price,squareMeter){
    console.log("价格="+price);
    console.log('squareMeter='+squareMeter);
})
salesOffices.trigger(2000000,88)
//价格=2000000
// squareMeter=88

下面为订阅的消息做一个key,添加上订阅者id,并把发布-订阅的功能提取出来作为一个对象。

var event = {
    clientList:{},
    listen:function(key,id,fn){
        // key 订阅表示 id 订阅者id fnu回调函数
        // 一个订阅标识下面可能存在很多个订阅者以及回调函数
        // 所以将订阅者作为数组,数组中存放回调函数
        if(!this.clientList[key]){
            this.clientList[key]=[];
        }
        this.clientList[key].push({
            id,fn
        })
    },
    trigger:function(...args){//rest参数
        var key = args.shift();
        fns = this.clientList[key];
       	// 回调函数数组为空 什么都不做
        if(!fns||fns.length==0) return;
        // 依次触发回调函数数组中的函数
        for(var i=0,fn;fn = fns[i];i++){
            // fn 是一个有id fn 属性的对象
            fn.fn.apply(this,args)
        }
    },
    remove:function(key,fn){
        var fns = this.clientList[key];
        if(!fns) return false//对应的key没有人订阅
        if(!fn){//没有传入具体的回调则取消key的所有订阅
            fns && (fns.length=0) 
        }else{
            for(var i = fns.length-1;i>=0;i--){
                var _fn = fns[i].fn;
                if(_fn===fn){
                    fns.splice(i,1)//删除回调
                }
            }
        }
    }
};

//定义一个installEvent函数为对象安装发布订阅功能
var installEvent = function(obj){
    for(var i in event){
        // 遍历event对象的key 为 obj 添加上属性和属性方法
        obj[i] = event[i]
    }
}
var salesOffices = {};
installEvent(salesOffices)
salesOffices.listen("square88",1,f1 = function(price){
    console.log(`square88的消息,价格是${price}`)
})
salesOffices.listen("square88",2,f3 = function(price){
    console.log(`square88的消息,价格是${price}`)
})
salesOffices.listen("square100",2,f2 = function(price){
    console.log(`square100的消息,价格是${price}`)
})
salesOffices.trigger('square88',1000000)//square88的消息,价格是1000000  *2
salesOffices.trigger('square100',1500000)//square100的消息,价格是1500000

salesOffices.remove("square88",f1);
salesOffices.trigger('square88',1000000)//square88的消息,价格是1000000
vue怎么监听对象的变化

Object.defineProperty

能自定义get和set函数,在获取和设置对象属性时可以触发对应回调函数。 利用这个方法,为对象中的每个属性安装发布订阅功能就可以了。

// 创建一个Vue 构造函数,使用 prototype 继承方法
function Vue(data){
    // new新的对象后会有data属性 
    this.data = data;
    // watchList 相当于 上面的clientList
    this.watchList = [];
    // 为data对象添加 发布消息的功能
    this.$bindObserver(data)
    
}
var $watch = function (key,fn){
    //监听的id是谁已经不重要,所以去掉了
    if(!this.clientList[key]){
        this.clientList[key]=[];
    }
    this.clientList[key].push(fn)
}

var $emit = function(...args){
    var key = args.shift();
    fns = this.watchList[key];
    if(!fns||fns.length==0) return;
    for(var i=0,fn;fn = fns[i];i++){
        fn.apply(this,args)
    }
}

var $remove = function(key,fn){
    var fns = this.clientList[key];
    if(!fns) return false
    if(!fn){
        fns && (fns.length=0) 
    }else{
        for(var i = fns.length-1;i>=0;i--){
            var _fn = fns[i].fn;
            if(_fn===fn){
                fns.splice(i,1)
            }
        }
    }
}

var $bindObserver = function(data){
var self = this;
var keys = Object.keys(data)
keys.forEach(key => {
    var result = data[key];
    //这里用到闭包 result 作为在函数内被内部函数引用的变量,一直存在于缓存中
    Object.defineProperty(data,key, {
        enumerable:true,
        configurable:true,
        get:function(){
            // return 的是上个作用域的result
            return result
        },
        set:function(newVal){
            self.$emit(key,newVal);
            result = newVal;
        }
    })
  })
}
Vue.prototype = {
    $watch,$emit,$remove,$bindObserver
}
var person={
    name:"yosgi",
    age:29
}
var app1 = new Vue(person)
app1.listen("age",function(val){
    console.log(`age被改变,值为${val}`)
})
app1.data.age = 30
age被改变值为30

上面实现的代码还有问题。

对象往往是一个深层次的结构,对象的某个属性可能仍然是一个对象,这种情况怎么处理? 应该用递归来处理

function Vue(data){
    this.data = data;
    this.watchList = [];
    //相当于clientLists
    this.$bindObserver(data)
}
var $watch = function (key,fn){
    if(!this.watchList[key]){
        this.watchList[key]=[];
    }
    this.watchList[key].push(fn)
}

var $emit = function(...args){
    var key = args.shift();
    console.log(this.watchList)
    fns = app1.watchList[key];
    if(!fns||fns.length==0) return;
    for(var i=0,fn;fn = fns[i];i++){
        fn.apply(this,args)
    }
}

var $remove = function(key,fn){
    var fns = this.watchList[key];
    if(!fns) return false
    if(!fn){
        fns && (fns.length=0) 
    }else{
        for(var i = fns.length-1;i>=0;i--){
            var _fn = fns[i].fn;
            if(_fn===fn){
                fns.splice(i,1)
            }
        }
    }
}

var $bindObserver = function(data){
var self = this;
var keys = Object.keys(data)
keys.forEach(key => {
    var result = data[key];
    if(typeof result ==='object'){
        self.$bindObserver(data[key])
    }
    Object.defineProperty(data,key, {
        enumerable:true,
        configurable:true,
        get:function(){
            return result
        },
        set:function(newVal){
            self.$emit(key,newVal);
             //如果newVal也是对象,同样需要对 对象添加 发布消息的功能
            if(typeof newVal ==='object'){
                self.$bindObserver(newVal)
            }
            result = newVal;
        }
    })
  })
}
Vue.prototype = {
    $watch,$emit,$remove,$bindObserver
}
var person={
    name:"yosgi",
    age:29,
    address:{
        city:"hangZhou",
        province:{
            a:1,
            b:2
        }
    }
}
var app1 = new Vue(person)

app1.$watch("city",function(val){
    console.log(`city被改变,值为${val}`)
})

app1.data.address.city = "beijing"
//city被改变,值为beijing

小结

发布订阅模式的优点是时间和对象之间的解耦。缺点是创建订阅者本身要消耗时间和内存,订阅一个消息后若消息始终没有发生,订阅者会始终存在与内存中。

vue监听对象变化是发布订阅模式的一种应用,其中发布者就是 watchList 订阅者,通过$bindObserver 发布消息 。$bindObserver的作用是监听对象属性的改变触发$emit。需要注意的是

1.可能存在嵌套对象,嵌套对象的属性需要有发布功能。

2.可能修改后的变量也是对象,改变新的对象的属性仍需要有发布的功能。