• 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 // 使用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 }

    对比 dealWithAsyncdealWithPromise两个简单的函数,能直观的发现:使用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 // 作为对象属性的简写式
    11 let o = {
    12  async A() {}
    13 };
    14 
    15 // 箭头函数
    16 let o = {
    17  A: async () => {}
    18 };
    1.2 返回值

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

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

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

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

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

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

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

    // 成功执行案例
    A1().then(res => {
     console.log('执行成功', res); // 约两秒后输出100。
    });
    
    async function A1() {
     let n1 = await 10;
     let n2 = await new Promise(resolve => {
       setTimeout(() => {
         resolve(10);
       }, 2000);
     });
     return n1 * n2;
    }
    
    // 失败执行案例
    A2().catch(err => {
     console.log('执行失败', err); // 约两秒后输出10。
    });
    
    async function A2() {
     let n1 = await 10;
     let n2 = await new Promise((resolve, reject) => {
       setTimeout(() => {
         reject(10);
       }, 2000);
     });
     return n1 * n2;
    }

    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 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 
    26 function createPromise(n = 10) {
    27  return new Promise(resolve => {
    28    setTimeout(() => {
    29      resolve(n);
    30    }, 1000);
    31  });
    32 }
    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函数最大改进便是内置了自执行器。

    // 两者都是每隔一秒钟打印出10,重复两次。
    // A函数
    A();
    async function A() {
     let n1 = await createPromise();
     console.log(n1);
     let n2 = await createPromise();
     console.log(n2);
    }
    
    // G函数,使用自执行器执行。
    spawn(G);
    
    function* G() {
     let n1 = yield createPromise();
     console.log(n1);
     let n2 = yield createPromise();
     console.log(n2);
    }
    
    function spawn(genF) {
     return new Promise(function(resolve, reject) {
       const gen = genF();
       function step(nextF) {
         let next;
         try {
           next = nextF();
         } catch(e) {
           return reject(e);
         }
         if(next.done) {
           return resolve(next.value);
         }
         Promise.resolve(next.value).then(function(v) {
           step(function() { return gen.next(v); });
         }, function(e) {
           step(function() { return gen.throw(e); });
         });
       }
       step(function() { return gen.next(undefined); });
     });
    }
    
    function createPromise() {
     return new Promise(resolve => {
       setTimeout(() => {
         resolve(10);
       }, 1000);
     });
    }
    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命令时(此处略去三五字),所以有了如此这般的执行顺序。谢幕。

  • 相关阅读:
    CentOS 阿里源
    使用分区挂载 ftp 目录
    Docker-compose常用命令
    docker 启动容器失败 id already in use
    Docker daemon.json 的配置项目合集
    Watchtower
    umount 时目标忙解决办法
    opencontrail 参考资料
    使用disk-image-builder(DIB)制作Ironic 裸金属镜像
    Nodejs常见安装
  • 原文地址:https://www.cnblogs.com/ljx20180807/p/9729596.html
Copyright © 2020-2023  润新知