一篇文章了解前端异步编程方案演变

5次阅读

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

对于 JS 而言,异步编程我们可以采用回调函数,事件监听,发布订阅等方案,在 ES6 之后,又新添了 Promise,Genertor,Async/Await 的方案。本文将阐述从回调函数到 Async/Await 的演变历史,以及它们之间的关系。
1. 异步编程的演变
首先假设要渲染一个页面,只能异步的串行请求 A,B,C,然后才能拿到页面的数据并请求页面
针对于不同的异步编程方式,我们会得到如下的代码:
1.1 回调函数
// 假设 request 是一个异步函数
request(A, function () {
request(B, function () {
request(C, function () {
// 渲染页面
})
})
})
回调函数的嵌套是愈发深入的。在不断的回调中,request(A) 回调函数中的其他逻辑会影响到 request(B),request(C) 中的逻辑,同理,request(B) 中的其他逻辑也会影响到 request(C)。在这个例子中,request(A) 调用 request(B),request(B) 调用 request(C),request(C) 执行完毕返回,request(B) 执行完毕返回,request(A) 执行完毕返回。我们很快会对先后顺序产生混乱,从而很难直观的分析出异步回调的结果。这就被称为回调地狱。
为了解决这种情况,ES6 新增了 Promise 对象。
1.2 Promise
// 假设 request 是一个 Promise 函数
request(A).then(function () {
return request(B)
}).then(function () {
return request(C)
}).then(function () {
// 渲染页面
})
Promise 对象用 then 函数来指定回调。所以,之前在 1.1 中回调函数的例子可以改为上文中的模样。可以看到,Promise 并没有消除回调地狱,但是却通过 then 链将代码逻辑变得更加清晰了。在这个例子中,request(A) 调用 request(B),request(B) 调用 request(C),request(C) 执行完毕返回。现在,request(A) 中的内容只能通过显示声明的 data 来影响到 request(C)——如果没有显示的在回调中声明,则影响不了 request(C),换言之,每段回调被近乎独立的分割了。
但是 Promise 本身还是有一堆的 then,还是不能让我们像写同步代码一样写异步的代码,因此 JS 又引入了 Generator。
1.3 Generator
function* gen(){
var r1 = yield request(A)
var r2 = yield request(B)
var r3 = yield request(C)
// 渲染页面
};
Generator 是协程在 ES6 上的实现,协程是指一个线程上不同函数间执行权可以相互切换。如本例,先执行 gen(),然后在遇到 yield 时暂停,执行权交给 request(A),等到调用了 next() 方法,再将执行权还给 gen()。
通过协程,JS 就实现了用同步的方式写异步的代码,但是 Generator 的使用要配合执行器,这自然是麻烦的。于是就有了 Async/Await。
Generator 的自动执行器是 co 函数库,有兴趣的同学可以通过阅读《co 函数库的含义和用法》来进行了解。
1.4 Async/Await
async function gen() {
var r1 = await request(A)
var r2 = await request(B)
var r3 = await request(C)
// 渲染页面
}
如果比较代码的话,1.4 的代码只是把 1.3 的代码中 * => async,yield 变为 await。但 Async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里 [1]。spawn 就是自动执行器。
async function fn(args){
// …
}

// 等同于

function fn(args){
return spawn(function*() {
// …
});
}

除此以外,Async 函数比 Generator 函数有更好的延展性——yield 接的是 Promise 函数 /Thunk 函数,但 await 还可以包括普通函数。对于普通函数,await 表达式的运算结果就是它等到的东西。否则若 await 等到的是一个 Promise 函数,await 就会协程到这个 Promise 函数上,直到它 resolve 或者 reject,然后再协程回主函数上 [2]。当然,Async 函数也比 Generator 函数更加易读和易理解。
2. 总结
本文阐述了从回调函数到 Async/Await 的演变历史。Async 函数作为换一个终极解决方案,尽管在并行异步处理上还要借助 Promise.all(),但其他方面已经足够完美。
参考文档

《深入掌握 ECMAScript 6 异步编程》系列
《理解 JavaScript 的 async/await》

正文完
 0