手写Promise02-发布订阅模式和观察者模式

470 阅读5分钟

关于JavaScript并发问题

通常来说,大家都在灌输一个概念,那就是JavaScript是单线程的,同一时间只能执行一个任务。但是这个并不准确。下面这段代码我们来模拟下并发环境

const fs=require('fs')
let arr=[]
// node都是基于回调的
fs.readFile('./a.txt','UTF-8',function(err,data){
    arr.push(data)
})
fs.readFile('./b.txt','UTF-8',function(err,data){
    arr.push(data)
})
console.log(arr)

按照单线程的思想,我们这边顺序读取了a.txt和b.txt之后,文本文件的内容他会写入到arr里面去。但是实际上并不是这样的。

当你答应arr的时候,你会发现arr的内容并没有发生改变,因为nodejs的回调函数都是在最后执行的。那么我们应该怎么办?这时有个替代的方案就是在回调函数里面写个语句调用外界的方法来提醒nodeJS,arr里面的内容发生了改变.

let arr=[]
function out(){
    if(arr.length==2){
        console.log(arr)
    }
}
fs.readFile('./a.txt','UTF-8',function(err,data){
    arr.push(data)
})
fs.readFile('./b.txt','UTF-8',function(err,data){
    arr.push(data)
})

回调函数每成功一次都调用一次out。我们可以看到data在arr中的内容已经可以正常答应出来了。但是这种方法可能会存在一种问题。那就是如果你要读取多个文件那么arr.length可能就会需要动态的改变。这种写法效率是非常慢的。我们就可以将out这个方法结合上篇文章学习的高阶函数来改变下

function after(times,callback){
    let arr=[]
    return (data)=>{
        arr.push(data) // 保证数据顺序,那么我们就需要用索引来实现。
        if(--items===0){ // 多个请求并发需要通过计数器来实现
            callback(arr)
        }
    }
}
let out=after(1,(arr)=>{
    console.log(arr)
})
fs.readFile('./a.txt','UTF-8',function(err,data){
    out(data)
})

这样我们就可以实现,每次读取文件函数完成之后都传递给我们的out方法,out方法会调用after,然后after里面会再次返回一个箭头函数接受参数data来实际接受out传递过去的data。当符合after这个function里面预设的条件,比如这里我们预设的是自定义的times==0.我们就会执行回调方法。最后处理我们读取到的文件数据。这样写就方便很多了。

上面的代码告诉我们多个请求并发执行需要通过计数器来决定处理数据。那么可能就会存在一个问题。如果说实际我们请求了3个文件。但是我们的计数器只设置了2.也就是说当计数器设置与实际并发不匹配的情况下。那么可能就会导致第三次并发执行的时候没有打印读取到的文件数据。造成数据丢失。

发布订阅模式

为了解决上面提出的问题。这里引入发布订阅模式。订阅-> 再发布

事件中心

发布订阅模式,核心就是把多个方法先暂存起来。最后依次执行。

let events={
    _events=[]
    on(fn){
        this._events.push(fn)
    },
    emit(data){
        this._events.forEach(fn=>fn(data))
    }
}
// 订阅有顺序,可以采用数组来处理。
event.on(()=>{
    console.log('每读取一次,就触发一次')
})
let arr=[]
events.on((data)=>{ 
    arr.push(data)
})
events.on((data)=>{
    if(arr.length==2){
        console.log('读取完毕')
    }
})
fs.readFile('./a.txt','UTF-8',function(err,data){
    events.emit(data)
})
fs.readFile('./b.txt','UTF-8',function(err,data){
    events.emit(data)
})

这个发布订阅模式主要解决的问题就是解耦合。然后关于以上代码。其实还有一个就是先订阅再发布还是先发布再订阅的问题。但是对于JS代码来说。这种在别的语言可能会因为代码执行顺序的不同而产生的问题在JS并不存在。你可以自行决定先发布再订阅。还是先订阅再发布。

发布订阅模式解决回调地狱问题

首先我假定我有一个需求。我需要将一个文件先打开。打开之后我们再去读取。读取之后我们要再去改变在这个文件,改变之后我们要将改变的结果保存到文件里面去。这种顺序执行并且都涉及异步访问的请求非常的复杂。我们的代码可能会写成这样:

function open(){
    function read(){
        function write(){
            function save(){
            }
        }
    }
}

引入了发布订阅模式之后我们可以改善下这种需求的写法。规定将执行函数依次放进_events数组中去。然后按照顺序和相应的回调依次执行。这样能够更好的改善回调地狱的可能性。

function open(){
    events.emit("open",1)
}
function read(params){
    events.emit("read",2)
}
function write(){
    events.emit("write",3)
}
function save(){
    events.emit("save",4)
}

这就是发布、订阅、消息中心的简单概念和实现。

基于发布订阅的观察者模式

观察者模式就是基于发布订阅的。就是他们的内部实现可能有点区别。原来的发布和订阅之间是没有什么关联关系的。

先明确,这种模式是基于类的。很多种设计模式都是基于类来实现的。

class Subject { // 被观察者的类
    constructor(name){
        this.name=name
        this.state='非常开心'
        this.observers=[] // 这里注册观察者列表
    }
    attach(o){
        this.observers.push(o)
    }
    setState(newState){
        this.state=newState
        this.observers.forEach(o=>o.update(this.name,newState))
    }
}
class Observer{ // 观察者的类
    constructor(name){
        this.name=name
    }
    update(s,state){
        console.log(this.name+':'+s+'当前的'+state)
    }
}
let s=new State('小宝宝')
let o1=new Observer('爸爸')
let o2=new Observer('妈妈')
s.attach(o1)
s.attach(o2)
s.setState('我不开兴')
s.setState('我又开心了')
// 引入vue中的概念
// 数据变了,视图要进行更新。