漫谈异步函数执行的前世今生
今天简单聊聊异步函数的演变史,不着重讲解语法本身,而是借此窥探一下演变过程,以及是基于什么原因一步步推进。
回调函数
回调函数是最简单处理异步的方式,之所以会有回调函数原因在于 JavaScript
是单线程的语言,如果遇到 io 输入就会阻塞在这里,体验感受较差,为了不影响性能刻意设计。
当然回调函数本身不是问题,例如我们点击提交按钮,在 jQuery
可能会这样写
$(".btn").on("click", () => {
// 验证通过,执行发送
$.ajax({
url: url,
data: data,
success: success,
dataType: dataType,
});
});
可以看到,在功能简单的情况下使用回调函数是最简洁的写法。
回调函数的问题在于需求的不断扩充,使得我们在回调函数不停的嵌套,且还要在每层考虑抛出异常的情况,想想就让人头大。
fn1(fn2(fn3(fn4())));
下面通过一个爬虫例子举例,我们需要获取某网站列表字段还有每个列表对应的详情字段。
const request = require("request");
const reptile = (fn) => {
request("xxx/list", function (error, response, body) {
if (error) {
return fn(error);
}
request("xxx/details", function (err, res, data) {
if (error) {
return fn(error);
}
fn(body, data);
});
});
};
首先我们看上面这段代码,因为是只请求两次所以看起来不算混乱,不过也可以发现两处问题:
- 回调函数内的命名,按照好的做法不应当存在遮蔽现象,内部的代码覆盖外层的变量,这样会导致引用和歧义
- 代码在朝这
>>>
的形式拓展
当然也可以通过职责分离来进行一定程度的缓解,例如上面代码我们把爬取列表和爬取详情分离开,通过一个主函数来进行组装,不过这样的作法也只是缓解,当程序越来越庞大维护和调试的成本也会越来越高。
Promise
Promise
是社区为了解决回调问题而出现的提案,ES6
的时候将其吸收到规范内,变成了语言内置的特性。
为了保持跟之前代码兼容,现在 Promise 的检测都是基于.then
方法。
在说 Promise 之前,我们来回忆一下它的特性
- Promise 创建之后不可取消
- Promise 只有三种状态:进行、完成和失败,状态变更后不会改变
- Promise 可以随时
.then
,每次 then 都返回一个新的 Promise 实例
上面的特性总结下来就是给我们一个统一的处理机制,可以随时调用不用担心错过,这个在回调函数是可能发生的,例如一个事件没有初始监听,在发生之后监听是不会监听到的。
还是拿上面的爬虫例子,看基于 Promise 写是不是更优雅一些
const rp = require("request-promise");
// 这里Promise原本的库不支持,所以改用一下Promise版本
const reptile = () => {
return rp("xxx/list").then((listData) => {
return rp("xxx/details").then((detailsData) => {
return {
listData,
detailsData,
};
});
});
};
reptile()
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
观察上面代码,可以明显看到
- 改善了
>>>
之前横向代码的发展,结构更加清晰 - 拥有了统一的错误处理,在请求列表如果失败,后续的.then 也不会执行
而且 Promise 包装起来也十分简单,下面以 wait 函数为例
const wait = (time) => new Promise((resolve) => setTimeout(resolve, time));
不过 Promise 也不完全都是优点,例如它只是把代码的书写结构从>>>
变成往下的延伸,代码结构多起来也是挺糟心。
Generator
Generator
是 ES6 新出现的 API,它的出现解决了两个问题:
- 给 Symbol.iterator 提供了简单的实现
- 给异步变成提供了新的思路
我们重点聊一聊第二点,还是上面的爬虫例子,看下用 Generator 应当如何书写,为了省去一些写执行器的过程,这里直接结合co
模块进行书写。
如果对这部分感兴趣,可以点击了解Generator 函数的异步应用,里面会一步步包含如何实现一个简单执行器。
const rp = require("request-promise");
// 这里Promise原本的库不支持,所以改用一下Promise版本
const co = require("co");
function* getList() {
return rp("xxx/list");
}
function* getDetails() {
return rp("xxx/details");
}
function* peptile() {
const listData = yield getList();
const detailsData = yield getDetails();
return {
listData,
detailsData,
};
}
co(peptile);
再来观察一下上面代码,是不是感觉已经接近同步函数的写法了,这就是 Generator 带来的影响。
不过它的缺点也很明显,没有内置执行器,需要额外自己编写。
async
ES2017
引用了 async
函数,它本身是 Generator 的语法糖,我们上面说 Generator 最大缺点就是没有自带执行器,async 函数出现弥补了这一缺陷。
- 它本身自带执行器,await 执行的可以是任何表达式,不需要一定基于 Promise
- 语法更加简洁,返回值是基于 Promise
还是爬虫的例子,这次我们改用 async 函数书写
const rp = require("request-promise");
// 这里Promise原本的库不支持,所以改用一下Promise版本
function getList() {
return rp("xxx/list");
}
function getDetails() {
return rp("xxx/details");
}
const peptile = async () => {
const listData = await getList();
const detailsData = await getDetails();
return {
listData,
detailsData,
};
};
从上面的代码不难看出 async 函数的出现,标志异步编程解决方案的最终成熟,结合 babel
现在就可以在低版本浏览器运行起来。
不过 async 虽然很好,但是也有一些实现不了的点,例如讲 Promise 的时候 wait 函数,用 async 函数就是实现不了,在解决问题一定要零和组合。
最后
简单回顾了一下 js 的异步史:回调 > Promise > Generator > async
。受限于篇幅并没有讲解太多,但是从异步的解决可以看到 JavaScript 正在走向成熟。
最后如果这篇文章有什么错误欢迎指点,如果对你有帮助也可以start
下。