几种处理JavaScript异步操作的办法

引言
js的异步操作 , 已经是一个老生常谈的话题 , 关于这个话题的文章随便google一下都可以看到一大堆 。那么为什么我还要写这篇东西呢?在最近的工作中 , 为了编写一套相对比较复杂的插件 , 需要处理各种各样的异步操作 。但是为了体积和兼容性 , 不打算引入任何的pollyfill , 甚至连babel也不允许使用 , 这也意味着只能以es5的方式去处理 。使用回调的方式对于解耦非常不利 , 于是找了别的方法去处理这个问题 。问题是处理完了 , 却也引发了自己的一些思考:处理js的异步操作 , 都有一些什么方法呢?
1. 回调函数
传说中的“callback hell”就是来自回调函数 。而回调函数也是最基础最常用的处理js异步操作的办法 。我们来看一个简单的例子:
首先定义三个函数:
function fn1 () {
console.log('Function 1')
}
function fn2 () {
setTimeout(() => {
console.log('Function 2')
}, 500)
}
function fn3 () {
console.log('Function 3')
}
其中fn2可以视作一个延迟了500毫秒执行的异步函数 。现在我希望可以依次执行fn1 , fn2 , fn3 。为了保证fn3在最后执行 , 我们可以把它作为fn2的回调函数:
function fn2 (f) {
setTimeout(() => {
console.log('Function 2')
f()
}, 500)
}
fn2(fn3)
可以看到 , fn2和fn3完全耦合在一起 , 如果有多个类似的函数 , 很有可能会出现fn1(fn2(fn3(fn4(...))))这样的情况 。回调地狱的坏处我就不赘述了 , 相信各位读者一定有自己的体会 。
2. 事件发布/订阅
发布/订阅模式也是诸多设计模式当中的一种 , 恰好这种方式可以在es5下相当优雅地处理异步操作 。什么是发布/订阅呢?以上一节的例子来说 , fn1 , fn2 , fn3都可以视作一个事件的发布者 , 只要执行它 , 就会发布一个事件 。这个时候 , 我们可以通过一个事件的订阅者去批量订阅并处理这些
class AsyncFunArr {
constructor (...arr) {
this.funcArr = [...arr]
}
next () {
const fn = this.funcArr.shift()
if (typeof fn === 'function') fn()
}
run () {
this.next()
}
}
const asyncFunArr = new AsyncFunArr(fn1, fn2, fn3)
然后在fn1 , fn2 , fn3内调用其next()方法:
function fn1 () {
console.log('Function 1')
asyncFunArr.next()
}
function fn2 () {
setTimeout(() => {
console.log('Function 2')
asyncFunArr.next()
}, 500)
}
function fn3 () {
console.log('Function 3')
asyncFunArr.next()
}
// output =>
// Function 1
// Function 2
// Function 3
可以看到 , 函数的处理顺序等于传入AsyncFunArr的参数顺序 。AsyncFunArr在内部维护一个数组 , 每一次调用next()方法都会按顺序推出数组所保存的一个对象并执行 , 这也是我在实际的工作中比较常用的方法
3. Promise
使用Promise的方式 , 就不需要额外地编写一个消息订阅者函数了 , 只需要异步函数返回一个Promise即可 。且看例子:
function fn1 () {
console.log('Function 1')
}
function fn2 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 500)
})
}
function fn3 () {
console.log('Function 3')
}
同样的 , 我们定义了三个函数 , 其中fn2是一个返回Promise的异步函数 , 现在我们希望按顺序执行它们 , 只需要按以下方式即可:
fn1()
fn2().then(() => { fn3() })
// output =>
// Function 1
// Function 2
// Function 3
使用Promise与回调有两个最大的不同 , 第一个是fn2与fn3得以解耦;第二是把函数嵌套改为了链式调用 , 无论从语义还是写法上都对开发者更加友好 。
4. generator
如果说Promise的使用能够化回调为链式 , 那么generator的办法则可以消灭那一大堆的Promise特征方法 , 比如一大堆的then() 。
function fn1 () {
console.log('Function 1')
}
function fn2 () {
setTimeout(() => {
console.log('Function 2')


推荐阅读