25

前言

上一篇文章介绍了js异步的底层基础--Event Loop模型,本文将介绍JS中传统的几种异步操作实现的模式。

正文

1.回调函数(callback)

回调函数是异步的最基本实现方式。

// 例子:回调函数
const f1 = (callback) => setTimeout(()=>{
  console.log('f1')  // 自身要执行的函数内容
  callback()
},1000)

const f2 = () =>{ console.log('f2') }
f1(f2)
  • 思路:将回调函数作为参数传入主函数,执行完主函数内容之后,执行回调函数
  • 优点:简单粗暴、容易理解
  • 缺点:

    • 代码耦合度太高,不利于代码维护
    • 有多层回调的情况下,容易引起回调地狱
    • 一般回调的触发点只有一个,例如fs.readFile等函数,只提供传入一个回调函数,如果想触发2个回调函数,就只能再用一个函数把这两个函数包起来
// 例子1:回调地狱,依次执行f1,f2,f3...
const f1 = (callback) => setTimeout(()=>{
  console.log('f1')
  callback()
},1000)


const f2 = (callback) =>setTimeout(()=>{
  console.log('f2')
  callback()
},1000)
...
// 假设还有f3,f4...fn都是类似的函数,那么就要不断的把每个函数写成类似的形式,然后使用下面的形式调用:
f1(f2(f3(f4)))  


// 例子2:如果想给`fs.readFile`执行2个回调函数callback1,callback2
// 必须先包起来
const callback3 = ()=>{
    callback1
    callback2
}
fs.readFile(filename,[encoding],callback3)

2.事件监听(Listener)

事件监听的含义是:采用事件驱动模式,让任务的执行不取决于代码的顺序,而取决于某个事件是否发生。先给出实现的效果:

const f1 = () => setTimeout(()=>{
  console.log('f1') // 函数体

  f1.trigger('done') // 执行完函数体部分 触发done事件
},1000)
f1.on('done',f2) // 绑定done事件回调函数
f1()
// 一秒后输出 f1,再过一秒后输出f2

接下来手动实现一下上面的例子,体会一下这种方案的原理:

const f1 = () => setTimeout(()=>{
  console.log('f1') // 函数体

  f1.trigger('done') // 执行完函数体部分 触发done事件
},1000)

/*----------------核心代码start--------------------------------*/
// listeners 用于存储f1函数各种各样的事件类型和对应的处理函数
f1.listeners = {}
// on方法用于绑定监听函数,type表示监听的事件类型,callback表示对应的处理函数
f1.on = function (type,callback){
    if(!this.listeners[type]){
        this.listeners[type] = []
    }
    this.listeners[type].push(callback) //用数组存放 因为一个事件可能绑定多个监听函数
}

// trigger方法用于触发监听函数 type表示监听的事件类型
f1.trigger = function (type){
    if(this.listeners&&this.listeners[type]){
        // 依次执行绑定的函数
        for(let i = 0;i < this.listeners[type].length;i++){
            const  fn = this.listeners[type][i]
            fn()
        }
    }
}
/*----------------核心代码end--------------------------------*/
const f2 = () =>setTimeout(()=>{
  console.log('f2')
},1000)
const f3 = () =>{ console.log('f3') }

f1.on('done',f2) // 绑定done事件回调函数
f1.on('done',f3) // 多个回调

f1()
// 一秒后输出 f1, f3,再一秒后输出f2

核心原理:

  1. listeners对象储存要监听的事件类型和对应的函数;
  2. 调用on方法时,往listeners中对应的事件类型添加回调函数;
  3. 调用trigger方法时,检查listeners中对应的事件,如果存在回调函数,则依次执行;

和回调相比,代码上的区别只是把原先执行callback的地方,换成了执行对应监听事件的回调函数。但是从模式上看,变成了事件驱动模型

  • 优点:避免了直接使用回调的高耦合问题,可以绑定多个回调函数
  • 缺点:由事件驱动,不容易看出执行的主流程

3.发布/订阅模式(Publish/Subscribe)

在刚刚事件监听的例子中,我们改造了f1,使它拥有了添加监听函数和触发事件的功能,如果我们把这部分功能移到另外一个全局对象上实现,就成了发布订阅者模式

// 消息中心对象
const Message = {
  listeners:{}
}

// subscribe方法用于添加订阅者 类似事件监听中的on方法 里面的代码完全一致
Message.subscribe = function (type,callback){
    if(!this.listeners[type]){
        this.listeners[type] = []
    }
    this.listeners[type].push(callback) //用数组存放 因为一个事件可能绑定多个监听函数
}

// publish方法用于通知消息中心发布特定的消息 类似事件监听中的trigger 里面的代码完全一致
Message.publish = function (type){
    if(this.listeners&&this.listeners[type]){
        // 依次执行绑定的函数
        for(let i = 0;i < this.listeners[type].length;i++){
            const  fn = this.listeners[type][i]
            fn()
        }
    }
}

const f2 = () =>setTimeout(()=>{
  console.log('f2')
},1000)

const f3 = () => console.log('f3')

Message.subscribe('done',f2) // f2函数 订阅了done信号
Message.subscribe('done',f3) // f3函数 订阅了done信号
const f1 = () => setTimeout(()=>{
  console.log('f1') 
  Message.publish('done')  // 消息中心发出done信号
},1000)
f1() 
// 执行结果和上面完全一样

如果认真看的话会发现,这里的代码和上一个例子几乎没有区别,仅仅是:

  1. 创建了一个Message全局对象,并且listeners移到该对象
  2. on方法改名为subscribe方法,并且移到Message对象上
  3. trigger方法改名为publish,并且移到Message对象上

这么做有意义吗?当然有。

  • 在事件监听模式中,消息传递路线:被监听函数f1与监听函数f2直接交流
  • 在发布/订阅模式中,是发布者f1和消息中心交流,订阅者f2也和消息中心交流

如图:
对比2种模式
消息中心的作用正如它的名字--承担了消息中转的功能,所有发布者和订阅器都只和它进行消息传递。有这个对象的存在,可以更方便的查看全局的消息订阅情况。

实质上,这也是设计模式中,观察者模式和发布/订阅者模式的区别。

4.Promise

Promise 是异步编程的一种解决方案,它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

注意,只是在es6原生提供了Promise对象,不代表Promise的设计是在es6才出现的。最典型的,当我们还在使用jquery$.ajax时,已经使用$.ajax().then().catch()时,就已经用到了Promise对象。因此这个也归为传统异步实现。

关于Promise详细内容,建议大家学习阮一峰老师的ES6教程,本文只介绍异步相关的核心内容

接下来同样地,用js模拟实现一个简单的Promise对象。

首先分析Promise的要点:

  1. 构造函数接受一个函数为参数,并且要接受resolve(reject)方法
  2. 可以通过resolvereject方法改变状态:resolve使状态从pending(进行中)变成、fulfilled(已成功);reject使状态变成rejected(已失败)
  3. then方法用于注册回调函数,并且返回值必须为Promise对象,这样才能实现链式调用(链式调用是指p.then().then().then()这样的形式)

根据上述分析,实现一个有thenresolve方法的简单Promise对象:

// 例子:手动实现简单Promise

function MyPromise(fn){
  this.status = 'pending' 
  this.resolves =[] //存放成功执行后的回调函数
  return fn(this.resolve.bind(this))// 这里必须bind,否则this对象会根据执行上下文改变
}

// then方法用于添加注册回调函数
MyPromise.prototype.then = function(fn){
  // 注册回调函数 并返回Promise.
  this.resolves.push(fn)
  return this
}

// resolve用于变更状态 并且触发回调函数,实际上resolve可以接受参数 这里简单实现就先忽略
MyPromise.prototype.resolve = function(){
  this.status = 'fulfilled' 
  if(this.resolves.length===0){
    return 
  }
  // 依次执行回调函数 并清空
  for(i=0;i<this.resolves.length;i++){
    const fn = this.resolves[i]
    fn()
  }
  this.resolves = [] //清空
  return this
}

// 使用写好的MyPromise做实验
const f1 = new MyPromise(resolve=>{
  setTimeout(()=>{
    console.log('f1 开始运行')
    resolve()
  },1000)
})

f1.then(()=>{
  setTimeout(()=>{
    console.log('f1的第一个then')
  },3000)
})

// 一个小思考,下面函数的执行输出是什么?
f1.then(()=>{
  setTimeout(()=>{
    console.log('f1的第一个then')
  },3000)
}).then(()=>{
  setTimeout(()=>{
    console.log('f1的第二个then')
  },1000)
})

以上就是Promise的核心思路。

总结

本文针对传统的几种异步实现方案做了说明。而ES6中新的异步处理方案Generatorasync/await会在后面补充。


如果觉得写得不好/有错误/表述不明确,都欢迎指出
如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处。如果有问题也欢迎私信交流,主页有邮箱地址
如果觉得作者很辛苦,也欢迎打赏一杯咖啡~


安歌
7k 声望5.5k 粉丝

目前就职于Ringcentral厦门,随缘答题, 佛系写文章,欢迎私信探讨.