一、什么是发布订阅模式?ajax
发布订阅模式,在咱们生活中是很是常见的一种,好比咱们常见的微信公众号订阅号,被订阅的公众号做者会将更新的文章发送给每一个订阅者,再好比咱们找中介买房子,告诉了中介咱们的需求(订阅),而后中介手上有了适合的房源后,将信息发送给全部订阅的人(发布)等。缓存
二、看一个最简单发布订阅的代码例子微信
let e = { arr:[], on(fn){ this.arr.push(fn); }, emit(){ this.arr.forEach(fn=>fn()); } } e.on(()=>{ console.log("haha") }) e.on(()=>{ console.log("是否是傻") }) function sendMsg(){ e.emit() } sendMsg()
咱们定义了一个对象e,它有两个方法,on:用于接收订阅的事件,将事件存储在变量arr中,emit:用于遍历全部订阅的事件,并执行app
而后在调用方法sendMsg()的时候,执行e.emit()方法。异步
还有前面咱们使用after函数读取文件的例子,同时去读取文件,当两个文件读取完成以后,须要打印出两个文件的内容,这个也可使用咱们的发布订阅来完成。代码以下:ide
const e = { arr:[], on(fn){ this.arr.push(fn); }, emit(){ this.arr.forEach(fn=>fn()); } } let info = {}; e.on(()=>{console.log("我读完了")}) e.on(()=>{ if(Object.keys(info).length==2){ console.log(info); } }); fs = require("fs"); fs.readFile("name.txt","utf8",(err,data)=>{ info["name"]=data; e.emit(); }) fs.readFile("age.txt","utf8",(err,data)=>{ info["age"]=data; e.emit(); })
上面这个例子中,我先订阅了两个方法,一个函数做用是输出我读完了,一个函数用于判断当前存储文件内容的对象是否有两项,有的话,说明已经读完了。而后让fs分别去读取各自的文件name.txt和age.txt,每次读取完成都调用发布函数e.emit(),而后去轮流执行前面订阅的两个方法函数
三、发布订阅模式的优势ui
1)发布订阅模式最大的优势就是,解耦。this
发布订阅模式能够取代对象之间的硬编码的通知机制。一个对象不用再显示的调用另外一个对象的某个接口。且当有新的订阅者出现时,发布者不用修改任何代码,一样发布者发生改变时,以前的订阅者也不会受到影响。编码
举一个生活中的实例,好比我和朋友一块儿开了一个店,雇佣了一个店员帮咱们看店,而后咱们想要了解店里的经营状况,因此但愿店员每成交一个订单就给咱们分别发一条短信通知。
要实现这个功能,咱们须要分三步走:
a)须要定义一个发布者,如例子中的店员
b)发布者须要一个缓存列表,用于存放回调函数,以便通知订阅者
c)发布者发布消息的时候,须要遍历缓存列表,依次触发订阅函数
const employee = { employer:[], // 用于存放我和朋友的回调函数 listen(fn){ this.employer.push(fn) }, trigger(...args){ this.employer.forEach(fn=>fn(...args)) } } employee.listen((salesAmount)=>{ console.log("我:",salesAmount); // 我:800 }) employee.listen((salesAmount)=>{ console.log("朋友: ", salesAmount); // 朋友:800 }) employee.trigger(800);
上面就实现了一个简单的发布订阅,可是过了一段时间后,店里生意变得愈来愈好,天天的订单愈来愈多,我和朋友天天会收到不少不少短信,因此但愿将模式修改一下,我和朋友决定分工合做,我收取全部订单金额大于200的订单信息,朋友接受全部订单金额小于200的订单信息。要实现这个功能,咱们须要在订阅的时候,传递一个参数key,说明咱们的需求,而后店员在发布的时候,也传递对应的key进行区分,代码以下:
const employee = { employer:[], // 存放订阅信息的缓存列表 listen(key, fn){ // 订阅消息 if(this.employer.indexOf(key)==-1){ this.employer[key] = []; // 若是尚未订阅过此类信息,给此类信息建立一个缓存列表 } this.employer[key].push(fn); // 订阅的消息,添加到缓存列表中 }, trigger(){ // 发布消息 let key = Array.prototype.shift.call(arguments); // 取出消息类型 lt200;gt200 let fns = this.employer[key] // 取出对应的方法 if(!fns || fns.length==0){ return; } fns.forEach(fn=>{ fn(arguments); // 执行订阅方法(这里的arguments是已经去掉了消息类型的剩余参数) }) } } employee.listen("lt200",(salesAmount)=>{ // 订阅 console.log("朋友: ",salesAmount); }) employee.listen("gt200",(salesAmount)=>{ console.log("我:",salesAmount); }) employee.trigger("lt200", 80); // 发布 employee.trigger("gt200",600);
这样,我和朋友就能够按照本身的须要分别收到信息了。可是有一天朋友以为,每天收那么多信息,太麻烦了,不想再收到信息了,怎么办呢?聪明的你确定想到了,既然有订阅,就应该有取消订阅啊,因此接下来,咱们须要给对象添加一个取消订阅的方法
employee.cancelListen = function(key, fn){ // 取消订阅 let func = this.employer[key]; if(!func){ // 说明对应的key没有人订阅 return false; } if(!fn){ // 取消订阅的人没有传递须要取消哪一个订阅,则将当前key下对应的全部订阅所有取消 fn.length = 0; }
for(let l=func.length-1;i>=0;l--){
if(fn==func[l]){
func.splice(l,1)
}
} }
function a(...args){
console.log("我",...args)
}
employee.listen("gt200",a)
console.log(employee.trigger("gt200",900)) // 我900
employee.cancelListen("gt200",a)
console.log(employee.trigger("gt200", 900)) // 此时没有输出,由于订阅已经被取消了
可是咱们的程序里面常常会有这种状况,因为ajax异步,可能在程序尚未订阅的时候,发布程序已经执行了,因此咱们须要先将发布事件暂存起来,等到有人来订阅的时候,再执行。
const event = { // 订阅 cache:{}, stackFn:{}, listen(key, fn){ if(!cache[key]){ cache[key] = []; } cache[key].push(fn); let stackArgs_ = stackFn[key]; if(!stackArgs_){ return; } stackArgs.forEach(args,=>{ fn(...args); }) }, trigger(){ let key = Array.prototype.shift.call(arguments);// 取出第一个参数 let fns = this.cache[key]; if(!fns || fns.length==0){ if(!this.stackFn[key]){ this.stackFn[key] = []; } this.stackFn[key].push(arguments); return; } fns.forEach(fn_=>{ fn_(...arguments) }) } }
完整代码以下:
1 const event = { 2 let Event, _default = "default"; 3 Event = function(){ 4 let each, _create, _remove, _listen, _trigger, namespaceCache, _shift=Array.prototype.shift; 5 each = function(ary, fn){ 6 ary.forEach(item=>{ 7 fn.call(item); 8 }); 9 } 10 _create = function(key, fn, cache){ 11 // 订阅 12 if(!cache[key]){ 13 cache[key] = []; 14 } 15 cache[key].push(fn); 16 } 17 18 _trigger = function(){ 19 let key = _shift(arguments); 20 let args = _shift(arguments); 21 let cache = _shift(arguments); 22 let self = this; 23 fns = cache[key]; 24 if(!fns || fns.length==0){ 25 return; 26 } 27 each(fns, function(){ 28 this.apply(self, args); 29 }) // 至关于 fns.forEach(fn=>{fn(...args)}) 30 31 _remove = function(key, fn, cache){ 32 // 取消订阅 33 let fns = cache[key]; 34 if(!fns || fns.length==0){ 35 return; 36 } 37 if(!fn){ 38 fns = []; 39 } 40 for(let i=0;i<fns.length;i++){ 41 if(fns[i]==fn){ 42 fns.shift(i,1); 43 } 44 } 45 } 46 _create = function(namespace){ 47 // 建立订阅 48 namespace = namespace||_default; 49 let ret, cache={}, offlineCache={}; 50 ret = { 51 listen:(key, fn)=>{ 52 // 建立订阅:1)调用event的订阅方法,执行订阅操做;2)因为程序的异步,可能会出现执行发布的时候,订阅还没有完成,因此须要在订阅的时候,查看是否有离线信息,若是有的话,执行离线信息 53 _listen(key, fn, cache); 54 offlineFn = offlineCache[key]; 55 if(!offlineFn){ 56 return 57 } 58 each(offlineFn, function(){ 59 this(); 60 }) // 至关于offlineFn.forEach(fn=>{fn(key, fn, cache)}) 61 offlineCache[key]= [];// 离线信息执行一次以后,就清除 62 }, 63 remove:(key, fn)=>{ 64 // 取消订阅,直接调用event的取消订阅便可 65 _remove(key, fn, cache); 66 }, 67 trigger:(key, ...args)=>{ 68 // 发布,调用event的_trigger 69 _trigger(key, args, cache); 70 // 判断是否有订阅,若是没有订阅,须要将它加入离线缓存中 71 let stack = cache[key] 72 if(stack && stack.length>0){ 73 return; 74 } 75 let fn = function(){ 76 return _trigger.apply(self, [key, args, cache]); 77 } 78 if(offlineCache && offlineCache[key].length==0){ 79 offlineCache[key] = []; 80 } 81 offlineCache[key].push(fn); 82 } 83 } 84 return namespace?(namespaceCache[namespace]?namespaceCache[namespace]:namespaceCache[namespace]=ret):ret; 85 } 86 return { 87 create:_create, 88 listen:function(obj, fn){ 89 let event = this.create(obj['namespace']); 90 event.listen(obj["key"], fn); 91 }, 92 remove:function(obj, fn){ 93 let event = this.create(obj['namespace']); 94 event.remove(obj['key'], fn); 95 } 96 trigger:function(obj, ...args){ 97 let event = this.create(obj["namespace"]); 98 event.trigger.apply(this, obj[key], ...args); 99 } 100 } 101 }(); 102 return Event; 103 })();