前言上一篇文章介绍了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核心原理:用listeners对象储存要监听的事件类型和对应的函数;调用on方法时,往listeners中对应的事件类型添加回调函数;调用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() // 执行结果和上面完全一样如果认真看的话会发现,这里的代码和上一个例子几乎没有区别,仅仅是:创建了一个Message全局对象,并且listeners移到该对象on方法改名为subscribe方法,并且移到Message对象上trigger方法改名为publish,并且移到Message对象上这么做有意义吗?当然有。在事件监听模式中,消息传递路线:被监听函数f1与监听函数f2直接交流在发布/订阅模式中,是发布者f1和消息中心交流,订阅者f2也和消息中心交流如图:消息中心的作用正如它的名字–承担了消息中转的功能,所有发布者和订阅器都只和它进行消息传递。有这个对象的存在,可以更方便的查看全局的消息订阅情况。实质上,这也是设计模式中,观察者模式和发布/订阅者模式的区别。4.PromisePromise 是异步编程的一种解决方案,它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。注意,只是在es6原生提供了Promise对象,不代表Promise的设计是在es6才出现的。最典型的,当我们还在使用jquery的$.ajax时,已经使用$.ajax().then().catch()时,就已经用到了Promise对象。因此这个也归为传统异步实现。关于Promise详细内容,建议大家学习阮一峰老师的ES6教程,本文只介绍异步相关的核心内容。接下来同样地,用js模拟实现一个简单的Promise对象。首先分析Promise的要点:构造函数接受一个函数为参数,并且要接受resolve(reject)方法可以通过resolve和reject方法改变状态:resolve使状态从pending(进行中)变成、fulfilled(已成功);reject使状态变成rejected(已失败)then方法用于注册回调函数,并且返回值必须为Promise对象,这样才能实现链式调用(链式调用是指p.then().then().then()这样的形式)根据上述分析,实现一个有then和resolve方法的简单Promise对象:// 例子:手动实现简单Promisefunction 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中新的异步处理方案Generator和async/await会在后面补充。如果觉得写得不好/有错误/表述不明确,都欢迎指出如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处。如果有问题也欢迎私信交流,主页有邮箱地址如果觉得作者很辛苦,也欢迎打赏一杯咖啡~