yliu

时来天地皆同力,运去英雄不自由

漫谈异步函数执行的前世今生


今天简单聊聊异步函数的演变史,不着重讲解语法本身,而是借此窥探一下演变过程,以及是基于什么原因一步步推进。

回调函数

回调函数是最简单处理异步的方式,之所以会有回调函数原因在于 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下。