ES8 async & await

简单谈谈

async 是什么?一句话,它就是 Generator 函数的语法糖。

既然是语法糖,那么先来对比下 async 函数和 Generator 函数的区别。以下是分别用 asyncGenerator 写的异步操作:

const fs = require('fs');

const readFile = function (fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (error, data) => {
            if (error) return reject(error);
            resolve(data);
        });
    });
};

// Generator 的写法 & 执行
const gen = function* () {
    const f1 = yield readFile('/etc/fstab');
    const f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
}
autoGen(gen())

// async 的写法 & 执行
const asyncReadFile = async function () {
    const f1 = await readFile('/etc/fstab');
    const f2 = await readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
}
asyncReadFile();

**注:**不清楚 autoGen 是什么的可以查看上一节

上诉代码,主要有以下几点区别:

类型Generatorasync函数标识*async暂停语句yieldawait执行方法调用自动执行方法(autoGen)直接调用

如果能理解 Generator 函数在异步中的使用的话,那么我相信,async 函数对于我们来说,简直简单的不能在简单了。那么既然用 Generator 函数能解决的东西,为什么又要多一个 async

和大多数语法糖出现一样,async 主要是为了更好的语义和更广的适用性。主要表现在以下几点:

  1. asyncawait ,比起 *yield ,语义更清楚了。 async 表示函数里有异步操作, await 表示紧跟在后面的表达式需要等待结果。
  2. yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象或原始类型的值(数值、字符串和布尔值,等同于同步操作)。
  3. async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。可以用 then 方法指定下一步的操作。

基本用法

async

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。

async function asy(){
    return 'this is async fun';
};

asy().then(res => console.log(res));
// this is async fun

上述例子可见 async 函数最终返回 Promise 其结果为函数的返回值。

async 函数有多种形式(与基本函数的表现形式一致):

// 函数声明
async function foo() {};

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(/* ... */);

// Class 的方法
class Storage {
    constructor() {
        this.cachePromise = caches.open('avatars');
    }
    
    async getAvatar(name) {
        const cache = await this.cachePromise;
        return cache.match(`/avatars/${name}.jpg`);
    }
}

const storage = new Storage();
storage.getAvatar('jake').then(/* ... */);

// 箭头函数
const foo = async () => {};

async 函数的错误处理

async 内部有未捕捉的异常时, async 函数返回的 Promise 将会是 reject 状态。

async function f() {
    throw new Error('出错了');
}

f().then(
    v => console.log(v),
    e => console.log(e)
);
// Error: 出错了

await

正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolvePromise 对象。

async function f() {
    let a = await 123;
    return a;
}

// 等同于
async function f() {
    let a = await Promise.resolve(123);
    return a;
}

f().then(v => console.log(v));
// 123

await 命令后跟的 Promise 状态变更为 fulfilled(resolve) 时,a 会被赋值,a 的值即为异步处理的结果。

当然如果 await 命令后的 Promise 的状态变更为 reject 并且没有对该 Promise 进行 catch 处理,那么 async 函数结束执行返回的 Promise 也将会变成 reject 状态。

async function f() {
    await Promise.reject('出错了');
    await Promise.resolve('hello world');   // 不会执行
}

f();
.then(v => console.log(v));
.catch(e => console.log(e));
// 出错了

错误处理

有两种方式可以在不影响 async 函数执行的情况下进行捕捉异常:

try...catch

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f();
.then(v => console.log(v));
// hello world

Promise.catch

async function f() {
  await Promise.reject('出错了').catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f();
.then(v => console.log(v));
// 出错了
// hello world

使用注意点

  1. await 命令后面的 Promise 对象,运行结果可能是 rejected ,所以最好把 await 命令放在 try...catch 代码块中,或直接后面跟一个 catch 方法进行异常捕捉。
  2. 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
  3. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

关于第二点,由于 async 函数的设计就是以一种同步的写法写异步函数,所以 await 是一个一个执行的,有顺序的关系,所以当一个函数体内有两个互相无关的异步请求的话,直接写 await 是达不成同时进行异步操作的效果的,所以得先使用 Promise.all 进行一层包装。

let [foo, bar] = await Promise.all([getFoo(), getBar()]);