Async:简洁优雅的异步之道

开发 前端
在异步处理方案中,目前最为简洁优雅的便是async函数(以下简称A函数)。经过必要的分块包装后,A函数能使多个相关的异步操作如同同步操作一样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。

[[244079]]

前言

在异步处理方案中,目前最为简洁优雅的便是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. // 使用A函数的主代码。  
  5. async function dealWithAsync() {  
  6. totalNum = await getListCount();  
  7. console.log('Get count', totalNum); 
  8. if (pageSize * (curPage - 1) > totalNum) {  
  9. curPage = 1;  
  10.  
  11. return getListData();  
  12.  
  13. // 使用Promise的主代码。  
  14. function dealWithPromise() {  
  15. return new Promise((resolve, reject) => {  
  16. getListCount().then(res => {  
  17. totalNum = res;  
  18. console.log('Get count', res);  
  19. if (pageSize * (curPage - 1) > totalNum) {  
  20. curPage = 1;  
  21.  
  22. return getListData()  
  23. }).then(resolve).catch(reject);  
  24. });  
  25.  
  26. // 开始执行dealWithAsync函数。  
  27. // dealWithAsync().then(res => {  
  28. // console.log('Get Data', res)  
  29. // }).catch(err => {  
  30. // console.log(err);  
  31. // });  
  32. // 开始执行dealWithPromise函数。  
  33. // dealWithPromise().then(res => {  
  34. // console.log('Get Data', res)  
  35. // }).catch(err => {  
  36. // console.log(err);  
  37. // });  
  38. function getListCount() { 
  39. return createPromise(100).catch(() => {  
  40. throw 'Get list count error' 
  41. });  
  42.  
  43. function getListData() {  
  44. return createPromise([], {  
  45. curPage: curPage,  
  46. pageSize: pageSize,  
  47. }).catch(() => {  
  48. throw 'Get list data error' 
  49. });  
  50.  
  51. function createPromise(  
  52. data, // Reback data  
  53. params = null, // Request params  
  54. isSucceed = true 
  55. timeout = 1000,  
  56. ) {  
  57. return new Promise((resolve, reject) => {  
  58. setTimeout(() => {  
  59. isSucceed ? resolve(data) : reject(data);  
  60. }, timeout);  
  61. });  
  62.  

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

1 登堂

1.1 形式

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

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

  1. // 声明式  
  2. async function A() {}  
  3. // 表达式  
  4. let A = async function () {};  
  5. // 作为对象属性  
  6. let o = {  
  7. A: async function () {}  
  8. };  
  9. // 作为对象属性的简写式  
  10. let o = {  
  11. async A() {}  
  12. };  
  13. // 箭头函数  
  14. let o = {  
  15. A: async () => {}  
  16. };  

1.2 返回值

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

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

  1. // 成功执行案例  
  2. A1().then(res => {  
  3. console.log('执行成功', res); // 10  
  4. });  
  5. async function A1() {  
  6. let n = 1 * 10;  
  7. return n;  
  8.  
  9. // 失败执行案例  
  10. A2().catch(err => {  
  11. console.log('执行失败', err); // i is not defined.  
  12. });  
  13. async function A2() {  
  14. let n = 1 * i;  
  15. return n;  
  16.  

1.3 await

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

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

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

***,A函数中的return x表达式,相当于return await x的简写。 

  1. // 成功执行案例  
  2. A1().then(res => {  
  3. console.log('执行成功', res); // 约两秒后输出100。  
  4. });  
  5. async function A1() {  
  6. let n1 = await 10;  
  7. let n2 = await new Promise(resolve => {  
  8. setTimeout(() => {  
  9. resolve(10);  
  10. }, 2000);  
  11. });  
  12. return n1 * n2;  
  13.  
  14. // 失败执行案例  
  15. A2().catch(err => {  
  16. console.log('执行失败', err); // 约两秒后输出10。  
  17. });  
  18. async function A2() {  
  19. let n1 = await 10;  
  20. let n2 = await new Promise((resolve, reject) => {  
  21. setTimeout(() => {  
  22. reject(10);  
  23. }, 2000);  
  24. });  
  25. return n1 * n2; 
  26.  

2 入室

2.1 继发与并发

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

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

  1. // 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。  
  2. async function A1() {  
  3. let n1 = await createPromise();  
  4. console.log('N1', n1);  
  5. let n2 = await createPromise();  
  6. console.log('N2', n2);  
  7. let n3 = await createPromise();  
  8. console.log('N3', n3);  
  9.  
  10. async function A2() {  
  11. for (let i = 0; i< 3; i++) {  
  12. let n = await createPromise();  
  13. console.log('N' + (i + 1), n);  
  14.  
  15.  
  16. function createPromise() {  
  17. return new Promise(resolve => {  
  18. setTimeout(() => {  
  19. resolve(10);  
  20. }, 1000);  
  21. });  
  22.  

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

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

  1. // 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。  
  2. async function A1() { 
  3. let res = await Promise.all([createPromise(), createPromise(), createPromise()]);  
  4. console.log('Data', res);  
  5.  
  6. async function A2() {  
  7. let res = [];  
  8. let reqs = [createPromise(), createPromise(), createPromise()];  
  9. for (let i = 0; i< reqs.length; i++) {  
  10. res[i] = await reqs[i];  
  11.  
  12. console.log('Data', res);  
  13.  
  14. async function A3() {  
  15. let res = [];  
  16. let reqs = [9, 9, 9].map(async (item) => {  
  17. let n = await createPromise(item);  
  18. return n + 1;  
  19. });  
  20. for (let i = 0; i< reqs.length; i++) {  
  21. res[i] = await reqs[i];  
  22.  
  23. console.log('Data', res);  
  24.  
  25. function createPromise(n = 10) {  
  26. return new Promise(resolve => {  
  27. setTimeout(() => {  
  28. resolve(n);  
  29. }, 1000);  
  30. });  
  31.  

2.2 错误处理

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

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

  1. // A1和A2都执行成,且返回值为10。  
  2. A1().then(console.log);  
  3. A2().then(console.log);  
  4. async function A1() {  
  5. let n;  
  6. n = await createPromise(true);  
  7. return n;  
  8.  
  9. async function A2() {  
  10. let n;  
  11. try {  
  12. n = await createPromise(false);  
  13. } catch (e) {  
  14. n = e;  
  15.  
  16. return n;  
  17.  
  18. function createPromise(needCatch) {  
  19. let p = new Promise((resolve, reject) => {  
  20. reject(10);  
  21. });  
  22. return needCatch ? p.catch(err => err) : p;  
  23.  

2.3 实现原理

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

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

  1. // G函数  
  2. function* request() {  
  3. let n = yield createPromise();  
  4.  
  5. // A函数  
  6. async function request() {  
  7. let n = await createPromise();  
  8.  
  9. function createPromise() {  
  10. return new Promise(resolve => {  
  11. setTimeout(() => {  
  12. resolve(10);  
  13. }, 1000);  
  14. });  
  15.  

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

  1. // 两者都是每隔一秒钟打印出10,重复两次。  
  2. // A函数  
  3. A();  
  4. async function A() {  
  5. let n1 = await createPromise();  
  6. console.log(n1);  
  7. let n2 = await createPromise();  
  8. console.log(n2);  
  9.  
  10. // G函数,使用自执行器执行。  
  11. spawn(G);  
  12. function* G() {  
  13. let n1 = yield createPromise();  
  14. console.log(n1); 
  15. let n2 = yield createPromise();  
  16. console.log(n2);  
  17.  
  18. function spawn(genF) {  
  19. return new Promise(function(resolve, reject) {  
  20. const gen = genF();  
  21. function step(nextF) {  
  22. let next 
  23. try {  
  24. next = nextF();  
  25. } catch(e) {  
  26. return reject(e);  
  27.  
  28. if(next.done) {  
  29. return resolve(next.value);  
  30.  
  31. Promise.resolve(next.value).then(function(v) {  
  32. step(function() { return gen.next(v); });  
  33. }, function(e) {  
  34. step(function() { return gen.throw(e); });  
  35. });  
  36.  
  37. step(function() { return gen.next(undefined); });  
  38. });  
  39.  
  40. function createPromise() {  
  41. return new Promise(resolve => {  
  42. setTimeout(() => { 
  43. resolve(10);  
  44. }, 1000);  
  45. });  
  46.  

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. function F(A) {  
  5. console.log(1);  
  6. A().then(console.log);  
  7. console.log(2);  
  8.  
  9. async function A1() {  
  10. console.log(3);  
  11. console.log(4);  
  12. return 5;  
  13.  
  14. async function A2() {  
  15. console.log(3);  
  16. let n = await 5;  
  17. console.log(4);  
  18. return n;  
  19.  
  20. async function A3() {  
  21. console.log(3);  
  22. let n = await createPromise();  
  23. console.log(4);  
  24. return n;  
  25.  
  26. function createPromise() {  
  27. return new Promise(resolve => {  
  28. setTimeout(() => {  
  29. resolve(9);  
  30. }, 2000);  
  31. });  
  32.  

从结果上可归纳出一些表面形态。执行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. function F(A) {  
  6. console.log(1);  
  7. A().then(console.log);  
  8. console.log(2);  
  9.  
  10. async function A() {  
  11. console.log(3);  
  12. let n = await createPromise();  
  13. console.log(4);  
  14. return n;  
  15.  
  16. function* G() {  
  17. console.log(3);  
  18. let n = yield createPromise();  
  19. console.log(4);  
  20. return n;  
  21.  
  22. function createPromise() {  
  23. return new Promise(resolve => {  
  24. setTimeout(() => {  
  25. resolve(9);  
  26. }, 2000);  
  27. });  
  28.  
  29. function spawn(genF) {  
  30. return new Promise(function(resolve, reject) {  
  31. const gen = genF();  
  32. function step(nextF) {  
  33. let next 
  34. try { 
  35.  next = nextF();  
  36. } catch(e) {  
  37. return reject(e);  
  38.  
  39. if(next.done) {  
  40. return resolve(next.value);  
  41.  
  42. Promise.resolve(next.value).then(function(v) {  
  43. step(function() { return gen.next(v); });  
  44. }, function(e) {  
  45. step(function() { return gen.throw(e); });  
  46. });  
  47. step(function() { return gen.next(undefined); });  
  48. });  
  49.  

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

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2015-07-30 14:45:19

java简洁

2017-10-24 15:28:27

PHP代码简洁SOLID原则

2022-08-31 08:19:04

接口returnCode代码

2021-05-06 20:03:00

JavaStream代码

2022-09-02 08:17:40

MapStruct代码工具

2024-03-28 14:29:46

JavaScript编程

2017-04-19 08:47:42

AsyncJavascript异步代码

2018-07-23 08:19:26

编程语言Python工具

2021-03-29 09:26:44

SpringBoot异步调用@Async

2017-08-02 14:17:08

前端asyncawait

2014-07-15 10:08:42

异步编程In .NET

2023-11-23 13:50:00

Python代码

2021-01-26 08:07:44

Node.js模块 Async

2023-08-04 08:52:52

Optional消灭空指针

2023-04-14 08:10:59

asyncawait

2016-12-14 15:05:08

C#异步编程

2021-06-28 08:10:59

JavaScript异步编程

2023-11-16 18:17:13

Python编程内置模块

2022-04-10 10:41:17

ESLint异步代码

2022-09-08 13:58:39

Spring高并发异步
点赞
收藏

51CTO技术栈公众号