前言
上一篇文章介绍了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.Promise
Promise 是异步编程的一种解决方案,它由社区最早提出和实现,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
对象:
// 例子:手动实现简单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中新的异步处理方案Generator
和async/await
会在后面补充。
如果觉得写得不好/有错误/表述不明确,都欢迎指出
如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处。如果有问题也欢迎私信交流,主页有邮箱地址
如果觉得作者很辛苦,也欢迎打赏一杯咖啡~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。