ES6 的async/await 函数

相对于回调函数来说,Promise是一种相对优雅的选择。那么有没有更好的方案呢?答案就是async/await。
优势主要体现在,级联调用,也就是几个调用依次发生的场景。

相对于Promise,async/await有什么优点?

比较场景: 级联调用,也就是几个调用依次发生的场景:

  • Promise主要用then函数的链式调用,一直点点点,是一种从左向右的横向写法。
    async/await从上到下,顺序执行,就像写同步代码一样。这更符合人编写代码的习惯
  • Promise的then函数只能传递一个参数,虽然可以通过包装成对象,但是这会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦。
    async/await没有这个限制,就当做普通的局部变量来处理好了,用let或者const定义的块级变量,想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余的工作。
  • Promise在使用的时候最好将同步代码和异步代码放在不同的then节点中,这样结构更加清晰。
    async/await整个书写习惯都是同步的,不需要纠结同步和异步的区别。当然,异步过程需要包装成一个Promise对象,放在await关键字后面,这点还是要牢记的。
  • Promise是根据函数式编程的范式,对异步过程进行了一层封装。
    async/await是基于协程的机制,是真正的“保存上下文,控制权切换 ... ... 控制权恢复,取回上下文”这种机制,是对异步过程更精确的一种描述。

进程、线程和协程的理解
上面的文章很好地解释了这几个概念的区别。
如果不纠结细节,可以简单地认为:进程 > 线程 > 协程;
协程可以独立完成一些与界面无关的工作,不会阻塞主线程渲染界面,也就是不会卡。
协程,虽然小一点,不过能完成我们程序员交给的任务。而且我们可以*控制运行和阻塞状态,不需要求助于高大上的系统调度,这才是重点。

  • async/await是基于Promise的,是进一步的一种优化。不过再写代码的时候,Promise本身的API出现得很少,很接近同步代码的写法。

async关键字使用时有哪些注意点?

async使用注意:

  • 有了这个async关键字,只是表明里面可能有异步过程,里面可以有await关键字。当然,全部是同步代码也没关系。当然,这时候这个async关键字就显得多余了。不是不能加,而是不应该加。
  • async函数,如果里面有异步过程,会等待;
    但是async函数本身会马上返回,不会阻塞当前线程。

可以简单认为,async函数工作在主线程,同步执行,不会阻塞界面渲染。
async函数内部由async关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回。

  • async函数的返回值是一个Promise对象,这个是和普通函数本质不同的地方。这也是使用时重点注意的地方
    (1)return newPromise();这个符合async函数本意;
    (2)return data;这个是同步函数的写法,这里是要特别注意的。这个时候,其实就相当于Promise.resolve(data);还是一个Promise对象。
    在调用async函数的地方通过简单的=是拿不到这个data的。
    那么怎么样拿到这个data呢?
    很简单,返回值是一个Promise对象,用.then(data => { })函数就可以。
    (3)如果没有返回,相当于返回了Promise.resolve(undefined);
  • await是不管异步过程的reject(error)消息的,async函数返回的这个Promise对象的catch函数就负责统一抓取内部所有异步过程的错误。
    async函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise对象的catch就能抓到这个错误。
  • async函数执行和普通函数一样,函数名带个()就可以了,参数个数随意,没有限制;也需要有async关键字。
    只是返回值是一个Promise对象,可以用then函数得到返回值,用catch抓去整个流程中发生的错误。

await关键字使用时有哪些注意点?

await使用注意:

  • 只能放在async函数内部使用,不能放在普通函数里面,否则会报错。
  • 后面放Promise对象,在Pending状态时,相应的协程会交出控制权,进入等待状态。这个是本质。
  • await是async wait的意思,wait的是resolve(data)消息,并把数据data返回。比如,下面代码中,当Promise对象由Pending变为Resolved的时候,变量a就等于data;然后再顺序执行下面的语句console.log(a);
    这真的是等待,真的是顺序执行,表现和同步代码几乎一模一样。
const a = await new Promise((resolve, reject) => {
    // async process ...
    return resolve(data);
});
console.log(a);
  • await后面也可以跟同步代码,不过系统会自动转化成一个Promise对象。
比如
const a = await 'hello world';
其实就相当于
const a = await Promise.resolve('hello world');
这跟同步代码
const a = 'hello world';是一样的,还不如省点事,去掉这里的await关键字。
  • await只关心异步过程成功的消息resolve(data),拿到相应的数据data。至于失败消息reject(error),不关心,不处理。
    当然对于错误消息的处理,有以下几种方法供选择:
    (1)让await后面的Promise对象自己catch
    (2)也可以让外面的async函数返回的Promise对象统一catch
    (3)像同步代码一样,放在一个try...catch结构中

async 和 await 在干什么

async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

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

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:var	est> node --harmony_async_await .
Promise { 'hello async' }

async函数会返回一个promise,并且Promise对象的状态值是resolved(成功的)。
如果你没有在async函数中写return,那么Promise对象resolve的值就是是undefined。
如果你写了return,那么return的值就会作为你成功的时候传入的值

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

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

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

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

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

await 到底在等啥

await是等待promise异步方法执行完成,不过按语法说明 ,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

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 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 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);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

async/await 的优势在于处理 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();

// c:var	est>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 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();

// c:var	est>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 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 方案的死穴—— 参数传递太麻烦了,看着就晕!

参考:
https://segmentfault.com/a/1190000007535316
https://www.jianshu.com/p/ffa5cbe9ab29
https://www.bilibili.com/video/BV1Pt411Y7NA?p=3