Async:简洁优雅的异步之道

Posted 会搬砖的程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Async:简洁优雅的异步之道相关的知识,希望对你有一定的参考价值。

前言

在异步处理方案中,目前最为简洁优雅的便是 async函数(以下简称A函数)。经过必要的分块包装后,A函数能使多个相关的异步操作如同同步操作一样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。它本质是 Generator函数的语法糖,通俗的说法是使用G函数进行异步处理的增强版。

尝试

学习A函数必须有 Promise基础,最好还了解 Generator函数,有需要的可查看延伸小节。

为了直观的感受A函数的魅力,下面使用 Promise和A函数进行了相同的异步操作。该异步的目的是获取用户的留言列表,需要分页,分页由后台控制。具体的操作是:先获取到留言的总条数,再更正当前需要显示的页数(每次切换到不同页时,总数目可能会发生变化),最后传递参数并获取到相应的数据。

  1. let totalNum = 0; // Total comments number.

  2. let curPage = 1; // Current page index.

  3. let pageSize = 10; // The number of comment displayed in one page.

  4.  

  5. // 使用A函数的主代码。

  6. async function dealWithAsync() {

  7.  totalNum = await getListCount();

  8.  console.log(‘Get count‘, totalNum);

  9.  if (pageSize * (curPage - 1) > totalNum) {

  10.    curPage = 1;

  11.  }

  12.  

  13.  return getListData();

  14. }

  15.  

  16. // 使用Promise的主代码。

  17. function dealWithPromise() {

  18.  return new Promise((resolve, reject) => {

  19.    getListCount().then(res => {

  20.      totalNum = res;

  21.      console.log(‘Get count‘, res);

  22.      if (pageSize * (curPage - 1) > totalNum) {

  23.        curPage = 1;

  24.      }

  25.  

  26.      return getListData()

  27.    }).then(resolve).catch(reject);

  28.  });

  29. }

  30.  

  31. // 开始执行dealWithAsync函数。

  32. // dealWithAsync().then(res => {

  33. //   console.log(‘Get Data‘, res)

  34. // }).catch(err => {

  35. //   console.log(err);

  36. // });

  37.  

  38. // 开始执行dealWithPromise函数。

  39. // dealWithPromise().then(res => {

  40. //   console.log(‘Get Data‘, res)

  41. // }).catch(err => {

  42. //   console.log(err);

  43. // });

  44.  

  45. function getListCount() {

  46.  return createPromise(100).catch(() => {

  47.    throw ‘Get list count error‘;

  48.  });

  49. }

  50.  

  51. function getListData() {

  52.  return createPromise([], {

  53.    curPage: curPage,

  54.    pageSize: pageSize,

  55.  }).catch(() => {

  56.    throw ‘Get list data error‘;

  57.  });

  58. }

  59.  

  60. function createPromise(

  61.  data, // Reback data

  62.  params = null, // Request params

  63.  isSucceed = true,

  64.  timeout = 1000,

  65. ) {

  66.  return new Promise((resolve, reject) => {

  67.    setTimeout(() => {

  68.      isSucceed ? resolve(data) : reject(data);

  69.    }, timeout);

  70.  });

  71. }

对比 dealWithAsyncdealWithPromise两个简单的函数,能直观的发现:使用A函数,除了有 await关键字外,与同步代码无异。而使用 Promise则需要根据规则增加很多包裹性的链式操作,产生了太多回调函数,不够简约。另外,这里分开了每个异步操作,并规定好各自成功或失败时传递出来的数据,近乎实际开发。

1 登堂

1.1 形式

A函数也是函数,所以具有普通函数该有的性质。不过形式上有两点不同:一是定义A函数时, function关键字前需要有 async关键字(意为异步),表示这是个A函数。二是在A函数内部可以使用 await关键字(意为等待),表示会将其后面跟随的结果当成异步操作并等待其完成。

以下是它的几种定义方式。

  1. // 声明式

  2. async function A() {}

  3.  

  4. // 表达式

  5. let A = async function () {};

  6.  

  7. // 作为对象属性

  8. let o = {

  9.  A: async function () {}

  10. };

  11.  

  12. // 作为对象属性的简写式

  13. let o = {

  14.  async A() {}

  15. };

  16.  

  17. // 箭头函数

  18. let o = {

  19.  A: async () => {}

  20. };

1.2 返回值

执行A函数,会固定的返回一个 Promise对象。

得到该对象后便可监设置成功或失败时的回调函数进行监听。如果函数执行顺利并结束,返回的P对象的状态会从等待转变成成功,并输出 return命令的返回结果(没有则为 undefined)。如果函数执行途中失败,JS会认为A函数已经完成执行,返回的P对象的状态会从等待转变成失败,并输出错误信息。

  1. // 成功执行案例

  2.  

  3. A1().then(res => {

  4.  console.log(‘执行成功‘, res); // 10

  5. });

  6.  

  7. async function A1() {

  8.  let n = 1 * 10;

  9.  return n;

  10. }

  11.  

  12. // 失败执行案例

  13.  

  14. A2().catch(err => {

  15.  console.log(‘执行失败‘, err); // i is not defined.

  16. });

  17.  

  18. async function A2() {

  19.  let n = 1 * i;

  20.  return n;

  21. }

1.3 await

只有在A函数内部才可以使用 await命令,存在于A函数内部的普通函数也不行。

引擎会统一将 await后面的跟随值视为一个 Promise,对于不是 Promise对象的值会调用 Promise.resolve()进行转化。即便此值为一个 Error实例,经过转化后,引擎依然视其为一个成功的 Promise,其数据为 Error的实例。

当函数执行到 await命令时,会暂停执行并等待其后的 Promise结束。如果该P对象最终成功,则会返回成功的返回值,相当将 awaitxxx替换成 返回值。如果该P对象最终失败,且错误没有被捕获,引擎会直接停止执行A函数并将其返回对象的状态更改为失败,输出错误信息。

最后,A函数中的 returnx表达式,相当于 returnawaitx的简写。

  1. // 成功执行案例

  2.  

  3. A1().then(res => {

  4.  console.log(‘执行成功‘, res); // 约两秒后输出100。

  5. });

  6.  

  7. async function A1() {

  8.  let n1 = await 10;

  9.  let n2 = await new Promise(resolve => {

  10.    setTimeout(() => {

  11.      resolve(10);

  12.    }, 2000);

  13.  });

  14.  return n1 * n2;

  15. }

  16.  

  17. // 失败执行案例

  18.  

  19. A2().catch(err => {

  20.  console.log(‘执行失败‘, err); // 约两秒后输出10。

  21. });

  22.  

  23. async function A2() {

  24.  let n1 = await 10;

  25.  let n2 = await new Promise((resolve, reject) => {

  26.    setTimeout(() => {

  27.      reject(10);

  28.    }, 2000);

  29.  });

  30.  return n1 * n2;

  31. }

2 入室

2.1 继发与并发

对于存在于JS语句( for, while等)的 await命令,引擎遇到时也会暂停执行。这意味着可以直接使用循环语句处理多个异步。

以下是处理继发的两个例子。A函数处理相继发生的异步尤为简洁,整体上与同步代码无异。

  1. // 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。

  2.  

  3. async function A1() {

  4.  let n1 = await createPromise();

  5.  console.log(‘N1‘, n1);

  6.  let n2 = await createPromise();

  7.  console.log(‘N2‘, n2);

  8.  let n3 = await createPromise();

  9.  console.log(‘N3‘, n3);

  10. }

  11.  

  12. async function A2() {

  13.  for (let i = 0; i< 3; i++) {

  14.    let n = await createPromise();

  15.    console.log(‘N‘ + (i + 1), n);

  16.  }

  17. }

  18.  

  19. function createPromise() {

  20.  return new Promise(resolve => {

  21.    setTimeout(() => {

  22.      resolve(10);

  23.    }, 1000);

  24.  });

  25. }

接下来是处理并发的三个例子。A1函数使用了 Promise.all生成一个聚合异步,虽然简单但灵活性降低了,只有都成功和失败两种情况。A3函数相对A2仅仅为了说明应该怎样配合数组的遍历方法使用 async函数。重点在A2函数的理解上。

A2函数使用了循环语句,实际是继发的获取到各个异步值,但在总体的时间上相当并发(这里需要好好理解一番)。因为一开始创建 reqs数组时,就已经开始执行了各个异步,之后虽然是逐一继发获取,但总花费时间与遍历顺序无关,恒等于耗时最多的异步所花费的时间(不考虑遍历、执行等其它的时间消耗)。

  1. // 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。

  2.  

  3. async function A1() {

  4.  let res = await Promise.all([createPromise(), createPromise(), createPromise()]);

  5.  console.log(‘Data‘, res);

  6. }

  7.  

  8. async function A2() {

  9.  let res = [];

  10.  let reqs = [createPromise(), createPromise(), createPromise()];

  11.  for (let i = 0; i< reqs.length; i++) {

  12.    res[i] = await reqs[i];

  13.  }

  14.  console.log(‘Data‘, res);

  15. }

  16.  

  17. async function A3() {

  18.  let res = [];

  19.  let reqs = [9, 9, 9].map(async (item) => {

  20.    let n = await createPromise(item);

  21.    return n + 1;

  22.  });

  23.  for (let i = 0; i< reqs.length; i++) {

  24.    res[i] = await reqs[i];

  25.  }

  26.  console.log(‘Data‘, res);

  27. }

  28.  

  29. function createPromise(n = 10) {

  30.  return new Promise(resolve => {

  31.    setTimeout(() => {

  32.      resolve(n);

  33.    }, 1000);

  34.  });

  35. }

2.2 错误处理

一旦 await后面的 Promise转变成 rejected,整个 async函数便会终止。然而很多时候我们不希望因为某个异步操作的失败,就终止整个函数,因此需要进行合理错误处理。注意,这里所说的错误不包括引擎解析或执行的错误,仅仅是状态变为 rejectedPromise对象。

处理的方式有两种:一是先行包装 Promise对象,使其始终返回一个成功的 Promise。二是使用 try.catch捕获错误。

  1. // A1和A2都执行成,且返回值为10。

  2. A1().then(console.log);

  3. A2().then(console.log);

  4.  

  5. async function A1() {

  6.  let n;

  7.  n = await createPromise(true);

  8.  return n;

  9. }

  10.  

  11. async function A2() {

  12.  let n;

  13.  try {

  14.    n = await createPromise(false);

  15.  } catch (e) {

  16.    n = e;

  17.  }

  18.  return n;

  19. }

  20.  

  21. function createPromise(needCatch) {

  22.  let p = new Promise((resolve, reject) => {

  23.    reject(10);

  24.  });

  25.  return needCatch ? p.catch(err => err) : p;

  26. }

2.3 实现原理

前言中已经提及,A函数是使用G函数进行异步处理的增强版。既然如此,我们就从其改进的方面入手,来看看其基于G函数的实现原理。A函数相对G函数的改进体现在这几个方面:更好的语义,内置执行器和返回值是 Promise

更好的语义。G函数通过在 function后使用 *来标识此为G函数,而A函数则是在 function前加上 async关键字。在G函数中可以使用 yield命令暂停执行和交出执行权,而A函数是使用 await来等待异步返回结果。很明显, asyncawait更为语义化。

  1. // G函数

  2. function* request() {

  3.  let n = yield createPromise();

  4. }

  5.  

  6. // A函数

  7. async function request() {

  8.  let n = await createPromise();

  9. }

  10.  

  11. function createPromise() {

  12.  return new Promise(resolve => {

  13.    setTimeout(() => {

  14.      resolve(10);

  15.    }, 1000);

  16.  });

  17. }

内置执行器。调用A函数便会一步步自动执行和等待异步操作,直到结束。如果需要使用G函数来自动执行异步操作,需要为其创建一个自执行器。通过自执行器来自动化G函数的执行,其行为与A函数基本相同。可以说,A函数相对G函数最大改进便是内置了自执行器。

  1. // 两者都是每隔一秒钟打印出10,重复两次。

  2.  

  3. // A函数

  4. A();

  5.  

  6. async function A() {

  7.  let n1 = await createPromise();

  8.  console.log(n1);

  9.  let n2 = await createPromise();

  10.  console.log(n2);

  11. }

  12.  

  13. // G函数,使用自执行器执行。

  14. spawn(G);

  15.  

  16. function* G() {

  17.  let n1 = yield createPromise();

  18.  console.log(n1);

  19.  let n2 = yield createPromise();

  20.  console.log(n2);

  21. }

  22.  

  23. function spawn(genF) {

  24.  return new Promise(function(resolve, reject) {

  25.    const gen = genF();

  26.    function step(nextF) {

  27.      let next;

  28.      try {

  29.        next = nextF();

  30.      } catch(e) {

  31.        return reject(e);

  32.      }

  33.      if(next.done) {

  34.        return resolve(next.value);

  35.      }

  36.      Promise.resolve(next.value).then(function(v) {

  37.        step(function() { return gen.next(v); });

  38.      }, function(e) {

  39.        step(function() { return gen.throw(e); });

  40.      });

  41.    }

  42.    step(function() { return gen.next(undefined); });

  43.  });

  44. }

  45.  

  46.  

  47. function createPromise() {

  48.  return new Promise(resolve => {

  49.    setTimeout(() => {

  50.      resolve(10);

  51.    }, 1000);

  52.  });

  53. }

2.4 执行顺序

在了解A函数内部与包含它外部间的执行顺序前,需要明白两点:一为 Promise的实例方法是推迟到本轮事件末尾才执行的后执行操作,详情请查看链接。二为 Generator函数是通过调用实例方法来切换执行权进而控制程序执行顺序,详情请查看链接。理解好A函数的执行顺序,能更加清楚的把握此三者的存在。

先看以下代码,对比A1、A2和A3方法的结果。

  1. F(A1); // 接连打印出:1 3 4 2 5。

  2. F(A2); // 接连打印出:1 3 2 4 5。

  3. F(A3); // 先打印出:1 3 2,隔两秒后打印出:4 9。

  4.  

  5. function F(A) {

  6.  console.log(1);

  7.  A().then(console.log);

  8.  console.log(2);

  9. }

  10.  

  11. async function A1() {

  12.  console.log(3);

  13.  console.log(4);

  14.  return 5;

  15. }

  16.  

  17. async function A2() {

  18.  console.log(3);

  19.  let n = await 5;

  20.  console.log(4);

  21.  return n;

  22. }

  23.  

  24. async function A3() {

  25.  console.log(3);

  26.  let n = await createPromise();

  27.  console.log(4);

  28.  return n;

  29. }

  30.  

  31. function createPromise() {

  32.  return new Promise(resolve => {

  33.    setTimeout(() => {

  34.      resolve(9);

  35.    }, 2000);

  36.  });

  37. }

从结果上可归纳出一些表面形态。执行A函数,会即刻执行其函数体,直到遇到 await命令。遇到 await命令后,执行权会转向A函数外部,即不管A函数内部执行而开始执行外部代码。执行完外部代码(本轮事件)后,才继续执行之前 await命令后面的代码。

归纳到此已成功一半,之后着手分析其成因。如果客官您对本楼有所了解,那一定不会忘记‘自执行器’这位大婶吧?估计是忘记了。A函数的本质就是带有自执行器的G函数,所以探究A函数的执行原理就是探究使用自执行器的G函数的执行原理。想起了?

再看下面代码,使用相同逻辑的G函数会得到与A函数相同的结果。

  1. F(A); // 先打印出:1 3 2,隔两秒后打印出:4 9。

  2. F(() => {

  3.  return spawn(G);

  4. }); // 先打印出:1 3 2,隔两秒后打印出:4 9。

  5.  

  6. function F(A) {

  7.  console.log(1);

  8.  A().then(console.log);

  9.  console.log(2);

  10. }

  11.  

  12. async function A() {

  13.  console.log(3);

  14.  let n = await createPromise();

  15.  console.log(4);

  16.  return n;

  17. }

  18.  

  19. function* G() {

  20.  console.log(3);

  21.  let n = yield createPromise();

  22.  console.log(4);

  23.  return n;

  24. }

  25.  

  26. function createPromise() {

  27.  return new Promise(resolve => {

  28.    setTimeout(() => {

  29.      resolve(9);

  30.    }, 2000);

  31.  });

  32. }

  33.  

  34. function spawn(genF) {

  35.  return new Promise(function(resolve, reject) {

  36.    const gen = genF();

  37.    function step(nextF) {

  38.      let next;

  39.      try {

  40.        next = nextF();

  41.      } catch(e) {

  42.        return reject(e);

  43.      }

  44.      if(next.done) {

  45.        return resolve(next.value);

  46.      }

  47.      Promise.resolve(next.value).then(function(v) {

  48.        step(function() { return gen.next(v); });

  49.      }, function(e) {

  50.        step(function() { return gen.throw(e); });

  51.      });

  52.    }

  53.    step(function() { return gen.next(undefined); });

  54.  });

  55. }

自动执行G函数时,遇到 yield命令后会使用 Promise.resolve包裹其后的表达式,并为其设置回调函数。无论该 Promise是立刻有了结果还是过某段时间之后,其回调函数都会被推迟到在本轮事件末尾执行。之后再是下一步,再下一步。同样的道理适用于A函数,当遇到 await命令时(此处略去三五字),所以有了如此这般的执行顺序。谢幕。

以上是关于Async:简洁优雅的异步之道的主要内容,如果未能解决你的问题,请参考以下文章

ES8(2017)async / await

优雅地 `async/await`

优雅得捕获async/await的异常

解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!

用 async/await 来处理异步

用 async/await 来处理异步