js异步从入门到放弃(二)- 传统的异步实现方案

4次阅读

共计 4715 个字符,预计需要花费 12 分钟才能阅读完成。

前言
上一篇文章介绍了 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 会在后面补充。

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

正文完
 0