前言
为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它的优势非常显著:
不增加新的语法,可以立刻适配几乎所有浏览器。
以队列的形式组织代码,易读好改。
捕获异常方案也基本可用。
这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。
现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。本场 Chat 我准备结合近期的开发经验,全面介绍 现代化的 JavaScript 异步开发。
目标读者要求
前端水平:初级、中级。
了解 JavaScript。
最好有异步开发经验,希望写出更好的代码。
名词及约定
ES6 = ES2015
ES7 = ES2016 + ES2017
异步函数 = Async Functions = Await/Async
范例代码会使用 ES6 的语法,也会混用 ES6 Module 和 CommonJS,请大家不要见怪。我会在代码当中加注释,其中会有一些关键内容,请大家不要忽略。
本文中所有代码均以 Node.js 7.0 为基础。
作者介绍
大家好,我叫翟路佳,花名“肉山”,这个名字跟 Dota 没关系,从高中起伴随我到现在。
我热爱编程,喜欢学习,喜欢分享,从业十余年,投入的比较多,学习积累到的也比较多,对前端方方面面都有所了解,希望能与大家分享。
我兴趣爱好比较广泛,尤其喜欢旅游,欢迎大家相互交流。
你可以在这里找到我:
反馈
如果您对于文中的内容有任何疑问,请在评论中告诉我。亦可发邮件给我:meathill[at]gmail.com。谢谢。
异步的问题
之所以会出现这样那样的解决方案,我之所以写这样的文章介绍这些解决方案,肯定是异步本身有问题。
是的,异步就是那样让人难以割舍,又那样让人不易亲近。
异步的起源
故事必须从头说起,在很久很久以前……
为校验表单,JavaScript 诞生了
在那个拨号上网的洪荒年代,浏览器还非常初级,与服务器进行数据交互的唯一方式就是提交表单。用户填写完成之后,交给服务器处理,如果内容合规当然好,如果不合规就麻烦了,必须打回来重填。那会儿网速还是论 Kb 的,比如我刚上网那会儿开始升级到 33.6Kb,主流还是 22.4Kb……
所以很容易想象:当用户填完100+选项,按下提交按钮,等待几十秒甚至几分钟之后,反馈回来的信息却是:“您的用户名不能包含大写字母”,他会有多么的崩溃多么的想杀人。为了提升用户体验,网景公司的布兰登·艾克大约用10天时间,开发出 JavaScript 的原型,从此,这门注定改变世界的语言就诞生了。
只是当时大家都还没有认识到这一点,发明它的目的,只是为校验表单。
JavaScript 中存在大量异步计算
同样为了提升用户体验,HTML DOM 也选择了边加载、边生成、边渲染的策略。再加上要等待用户操作,大量交互都以事件来驱动。于是,JavaScript 里很早就存在着大量的异步计算。
这也带来一个好处,作为一门 UI 语言,异步操作帮 JavaScript 避免了页面冻结。
为什么异步操作可以避免界面冻结呢?
同步的利弊
假设你去到一家饭店,自己找座坐下了,然后招呼服务员拿菜单来。
服务员说:“对不起,我是‘同步’服务员,我要服务完这张桌子才能招呼你。”
那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等待别人的一个大动作完成。你是不是很想抽ta?
这就是“同步”的问题:顺序交付的工作1234,必须按照1234的顺序完成。
不过“同步”也有“同步”的好处:逻辑非常简单。你不用担心每步操作会消耗多少时间,反正每一步操作都会在上一步完成之后才进行,只管往后写就是了。
异步的利弊
与之相反,异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫“异步”。
那些需要大量计算(比如 Service Worker),或者复杂查询(比如 Ajax)的工作,JS 引擎把它们交给系统之后,就立刻返回继续待机了,于是再进行什么操作,浏览器也能第一时间响应,这让用户的感觉非常好。
有利必有弊,异步的缺点就是:必须通过特殊的语法才能实现,而这些语法就不如同步那样简单、清晰、明了。
异步计算的实现
异步计算有两种常见的实现形式。
事件侦听
这种形式在浏览器里比较常见,比如,我们可以对一个 <button>
的用户点击行为增加侦听,在点击事件触发后调用函数进行处理。
document.getElementById('#button').addEventListener('click', function (event) {
// do something
}, false);
也可以使用 DOM 节点的 onclick
属性绑定侦听函数:
document.getElementById('#button').onclick = function (event) {
// do something
}
回调
到了 Node.js(以及其它 Hybrid 环境),由于要和引擎外部的环境进行交互,大部分操作都变成回调。比如用 fs.readFile()
读取文件内容:
const fs = require('fs');
fs.readFile('path/to/file.txt', 'utf8', (err, content) => {
if (err) {
throw err;
}
console.log(content);
});
如果你不熟悉 Node.js 也没关系,jQuery 里也有类似的操作,最常见的就是侦听页面加载状态,加载完成后启动回调函数:
$(function () {
// 绑定事件
// 创建组件
// 以及其它操作
});
回调陷阱
这个问题其实是最直观的问题,也是大家谈的最多的问题。比如下面这段代码:
a(function (resultA) {
b(resultA, function (resultB) {
c(resultB, function (resultC) {
d(resultC, function (resultD) {
e(resultD, function (resultE) {
f(resultE, function (resultF) {
// 子子孙孙无穷尽也
console.log(resultF);
});
});
});
});
});
});
嵌套层次之深令人发指。这种代码很难维护,有人称之为“回调地狱”,有人称之为“回调陷阱”,还有人称之为“回调金字塔”,其实都无所谓,带来的问题很明显: