保存成功
订阅成功
保存失败,请重试
提交成功

JavaScript 异步开发攻略

大家好,我叫翟路佳,花名“肉山”,这个名字跟 Dota 没关系,从高中起伴随我到现在。 我热爱编程,喜欢学习,喜欢分享,从业十余年,投入的比较多,学习积累到的也比较多,对前端方方面面都有所了解,希望能与大家分享。 我兴趣爱好比较广泛,尤其喜欢旅游,欢迎大家相互交流。 你可以在这里找到我: 博客:https://blog.meathill.com 微博:https://weibo.com/meathill GitChat:https://gitbook.cn/gitchat/author/593cb520ef8d9c2863173543
查看本场Chat

前言

为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它的优势非常显著:

  1. 不增加新的语法,可以立刻适配几乎所有浏览器。

  2. 以队列的形式组织代码,易读好改。

  3. 捕获异常方案也基本可用。

这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。

现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。本场 Chat 我准备结合近期的开发经验,全面介绍 现代化的 JavaScript 异步开发。

目标读者要求

  1. 前端水平:初级、中级。

  2. 了解 JavaScript。

  3. 最好有异步开发经验,希望写出更好的代码。

名词及约定

  • 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);
          });
        });
      });
    });
  });
});

嵌套层次之深令人发指。这种代码很难维护,有人称之为“回调地狱”,有人称之为“回调陷阱”,还有人称之为“回调金字塔”,其实都无所谓,带来的问题很明显:

互动评论
评论
KING2 年前
好文,而且是长文,promise讲解中用到的读取文件的例子非常棒,但需要有一些node和promise基础; 同时由于主要讲promise的最佳用法,感觉有些东西没能细致的讲理论和原理,比如thenable是啥,这些还是得自己去琢磨和研究。
评论
灌汤包3 年前
看不明白
评论
Raven2 年前
说明你基础太差了 赶紧先去恶补下
评论
查看更多