Async/Await

Async/Await

  async/await是写异步代码的新方式,以前的方法有回调函数和Promise。
  async/await是基于Promise实现的,它不能用于普通的回调函数。
  async/await与Promise一样,是非阻塞的。
  async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

  是es7中提出来的异步解决方法,是目前解决异步编程终极解决方案,以promise为基础,其实也就是generator的高级语法糖,本身自己就相当于一个迭代生成器(状态机),它并不需要手动通过  next()来调用自己,与普通函数一样。async就相当于generator函数中的*,await相当于yield,

async 函数是什么

一句话,async 函数就是 Generator 函数的语法糖。()

async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

async 作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。举个例子:

// async函数返回的是一个Promise对象,async函数(包括函数语句、函数表达式、Lambda表达式)会返回一个Promise对象,如果在函数中return一个直接量,async会把这个直接量通过Promise.resolve() 封装成 Promise 对象

async function timeout( ) {
    return setTimeout(() => console.log(666),1000)
}
timeout();
console.log('虽然在后面,但是我先执行');

async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

async function testAsync( ) {
  return 'hello async';
}

testAsync().then((v) => {
  console.log(v);    // 输出 "hello async"
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。

await

await 表达式会 暂停 当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。

若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值 本身

function getSomething( ) {
  return 'something';
}

async function testAsync( ) {
  return Promise.resolve('hello async');
}

async function test( ) {
  const v1 = await getSomething();
  const v2 = await testAsync();
  console.log(v1, v2);
}

test();

记住,await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。 注意,当异步函数暂停时,它调用的函数会继续执行(收到异步函数返回的隐式Promise)。

async/await 帮我们干了啥

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写:

function takeLongTime( ) {
  return new Promise(resolve => {
    setTimeout(() => resolve('long_time_value'), 1000);
  });
}

takeLongTime().then((v) => {
    console.log('got', v);
});

如果改用 async/await 呢,会是这样:

function takeLongTime( ) {
  return new Promise(resolve => {
    setTimeout(() => resolve('long_time_value'), 1000);
  });
}

takeLongTime().then((v) => {
    const v = await takeLongTime();
    console.log(v);
});

优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/** * 传入参数 n,表示这个函数执行的时间(毫秒) * 执行的结果是 n + 200,这个值将用于下一步骤 */
function takeLongTime(n) {
  return new Promise(resolve => {
    setTimeout(() => resolve(n + 200), n);
  });
}

function step1(n) {
  console.log(`step1 with ${n}`);
  return takeLongTime(n);
}

function step2(n) {
  console.log(`step2 with ${n}`);
  return takeLongTime(n);
}

function step3(n) {
  console.log(`step3 with ${n}`);
  return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt( ) {
    console.time('doIt');
    const time1 = 300;
    step1(time1)
      .then(time2 => step2(time2))
      .then(time3 => step3(time3))
      .then(result => {
        console.log(`result is ${result}`);
        console.timeEnd('doIt');
      });
}

doIt();

// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1503.638916015625ms

如果用 async/await 来实现呢,会是这样:

async function doIt( ) {
    console.time('doIt');
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd('doIt');
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样。

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果:

function step1(n) {
  console.log(`step1 with ${n}`);
  return takeLongTime(n);
}

function step2(m, n) {
  console.log(`step2 with ${m} and ${n}`);
  return takeLongTime(m + n);
}

function step3(k, m, n) {
  console.log(`step3 with ${k}, ${m} and ${n}`);
  return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt( ) {
    console.time('doIt');
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd('doIt');
}

doIt();

// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2903.52001953125ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt( ) {
  console.time('doIt');
  const time1 = 300;
  step1(time1)
    .then(time2 => {
      return step2(time1, time2)
        .then(time3 => [time1, time2, time3]);
    })
    .then(times => {
      const [time1, time2, time3] = times;
      return step3(time1, time2, time3);
    })
    .then(result => {
      console.log(`result is ${result}`);
      console.timeEnd('doIt');
    });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

注意点

大多数人的误区

async function async1( ){
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2( ){
  console.log('async2');
}
async1();
console.log('i am koala');

我想会有一些开发者认为await是把同步变为异步,执行顺序是这样

"async1 start"
"async2"
"async1 end"
"i am koala"

然而并不是,正确的执行顺序是

"async1 start"
"async2"
"i am koala"
"async1 end"

解释一下原因:

“ async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。” ——阮一峰ES6

看不懂打印顺序的话,可以看 这个博客

运行结果是 rejected

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

/** * await处理catch异常情况 * @param {Promise} promise promise对象 * @returns {Array} */
function awaitWrap(promise) {
  if (!promise || !Promise.prototype.isPrototypeOf(promise)) {
    return new Promise((resolve, reject) => {
      reject(new Error('requires promises as the param'));
    }).catch((err) => {
      return [null, err];
    });
  }
  return promise
    .then(function( ) {
      return [...arguments, null];
    }).catch(err => {
      return [null, err];
    });
}

async function testAsync( ) {
  return Promise.resolve('hello async');
}

async function test( ) {
  const [res, err] = await awaitWrap(testAsync());

  console.log(res, err);
}

test();

async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。

协程

协程,又称微线程,纤程。英文名Coroutine。 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。

  • 进程>线程>协程
  • 协程的第一大优势是具有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
  • 协程的第二大优势是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多;
  • 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行,需要注意的是:在一个子程序中中断,去执行其他子程序,这并不是函数调用,有点类似于CPU的中断;
  • 用汽车和公路举个例子:js公路只是单行道(主线程),但是有很多车道(辅助线程)都可以汇入车流(异步任务完成后回调进入主线程的任务队列);generator把js公路变成了多车道(协程实现),但是同一时间只有一个车道上的车能开(依然单线程),不过可以自由变道(移交控制权);
  • 协程意思是多个线程互相协作,完成异步任务,运行流程大致如下:
    • 1)协程A开始执行;
    • 2)协程A执行到一半,进入暂停,执行权转移到协程B;
    • 3)一段时间后,协程B交还执行权;
    • 4)协程A恢复执行;
  • 协程是一个无优先级的子程序调度组件,允许子程序在特定的地点挂起恢复;
  • 线程包含于进程,协程包含于线程,只要内存足够,一个线程中可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源;
  • 就实际使用理解来说,协程允许我们写同步代码的逻辑,却做着异步的事,避免了回调嵌套,使得代码逻辑清晰;
  • 何时挂起,唤醒协程:协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程,那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时;
  • 单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:
    • 1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
    • 2)单线程内就可以实现并发的效果,最大限度地利用cpu;
posted @ 2020-03-04 16:32  太阳锅锅  阅读(590)  评论(0编辑  收藏  举报