異步發展流程 —— 異步編程的終極大招 async/await

在這裏插入圖片描述

閱讀原文


前言

這篇文章是異步發展流程系列的最後一篇,可能會涉及 Promise、Generators、co 等前置知識,若是對這些不是很瞭解能夠看這個系列的前三篇:編程

若是已經具有這些前置知識,那咱們繼續看看今天的主角,JavaScript 異步編程的終極大招 async/await數組

async/await 簡介

async/await 指的是兩個關鍵字,是 ES7 引入的新標準,async 關鍵字用於聲明 async 函數,await 關鍵字用來等待異步(必須是 Promise)操做,說白了 async/await 就是 Generators + co 的語法糖。promise

async/await 和 Generators + co 的寫法很是的類似,只是把用於聲明 Generator 函數的 * 關鍵字替換成了 async 並寫在了 function 關鍵字的前面,把 yield 關鍵字替換成了 await;另外,async 函數是基於 Promise 的,await 關鍵字後面等待的異步操做必須是一個 Promise 實例,固然也能夠是原始類型的值,只不過這時的執行效果等同於同步,與 Generator 不一樣的是,await 關鍵字前可使用變量去接收這個正在等待的 Promise 實例執行後的結果。瀏覽器

async 函數的基本用法

async 函數返回一個 Promise 實例,可使用 then 方法添加回調函數。當函數執行的時候,只要遇到 await 就會等待,直到 await 後面的同步或異步操做完成,再接着執行函數體內後面的語句。併發

一、async 函數聲明

async 的聲明方式大概有如下幾種:框架

//  async 函數聲明
// 函數聲明
async function fn() {}

// 函數表達式
const fn = async function() {};

// 箭頭函數
const fn = async () => {};

// 做爲對象的方法
let obj = {
    async fn() {}
};

// 做爲 class 的方法
class Person(name) {
    constructor () {
        this.name = name;
    }
    async getName() {
        const name = await this.name;
        return name;
    }
}

在上一篇介紹 Generators + co 的文章中咱們舉了一個例子,使用 NodeJS 的 fs 模塊連續異步讀文件,第一個文件名爲 a.txt,讀到的內容爲 b.txt,做爲要讀的第二個文件的文件名,繼續讀 b.txt 後將讀到的內容 「Hello world」 打印出來。異步

咱們來使用 async/await 的方式來實現一下:async

// async 函數實現文件讀取
// 引入依賴
const fs = require("fs");
const util = require("util");

// 將 fs.readFile 轉換成 Promise
const readFile = util.promisify(fs.readFile);

// 聲明 async 函數
async function read(file) {
    let aData = await readFile(file, "utf8");
    let bData = await readFile(aData, "utf8");
    return bData;
}

// 調用 async 函數
read("a.txt").then(data => {
    console.log(data); // Hello world
});

其實對比上一篇文章 Generator 的案例,與 Generator 函數同樣,寫法像同步,執行是異步,不一樣的是咱們即沒有手動調用 next 方法,也沒有藉助 co 庫,實際上是 async 函數內部集成了相似於 co 的執行器,幫咱們在異步完成後自動向下執行代碼,因此說 async/await 是 Generators + co 的語法糖。異步編程

二、async 函數錯誤處理

async 函數內部若是執行錯誤能夠有三種方式進行錯誤處理:函數

  • await 後面的 Promise 實例使用 then 方法錯誤的回調或 catch 方法進行錯誤處理;
  • 若是有多個 await,能夠在 async 函數執行完後使用 catch 方法統一處理;
  • 因爲 async 內部代碼是同步的寫法,多個 await 的狀況也可使用 try...catch... 進行處理。

須要注意的是,若是在 async 函數內部使用了 try...catch... 又在函數執行完後使用了 catch,錯誤會優先被同步的 try...catch... 捕獲到,後面的 catch 就不會再捕獲了。

// async 函數異常捕獲
// 第一種
async function fn() {
    let result = await Promise.reject("error").catch(err => {
        console.log(err);
    });
}

fn(); // error

// 第二種
async function fn() {
    try {
        let val1 = await Promise.reject("error");
        let val2 = await Promise.resolve("success");
    } catch (e) {
        console.log(e);
    }
}

fn(); // error

// 第三種
async function fn() {
    let val1 = await Promise.resolve("success");
    let val2 = await Promise.reject("error");
}

fn().catch((err => console.log(err))); // error

三、await 異步併發

async 函數中,若是有多個 await 互不依賴,這種狀況下若是執行一個,等待一個完成,再執行一個,再等待完成,這樣是很浪費性能的,因此咱們要把這些異步操做同時觸發。

假設咱們異步讀取兩個文件,且這兩個文件不相關,我可使用下面的方式來實現:

// await 異步併發
// 前置
const fs = require("fs");
const util = require("util");
const readFile = util.promisify(fs.readFile);

// 須要改進的 async 函數
async function fn() {
    let aData = await readFile("a.txt", "utf8");
    let bData = await readFile("b.txt", "utf8");
    return [aData, bData];
}

fn();

// 在 async 函數外部觸發異步
let aDataPromise = readFile("a.txt", "utf8");
let bDataPromise = readFile("b.txt", "utf8");

async function fn() {
    let aData = await aDataPromise;
    let bData = await bDataPromise;
    return [aData, bData];
}

fn();

// 使用 Promise.all
async function fn() {
    let dataArr = await Promise.all(
        readFile("a.txt", "utf8"),
        readFile("a.txt", "utf8")
    );
    return dataArr;
}

fn();

四、使用 async/await 的注意點

使用 async/await 應注意如下幾點:

  • await 習慣性錯誤處理;
  • await 命令後互不依賴的異步應同時觸發;
  • async 函數中,函數的執行上/下文發生變化時,不能使用 await(如使用 forEach 循環的回調中)。

針對第一點,在 async 函數中 await 命令後面大多狀況下是 Promise 異步操做,運行結果可能出現錯誤並調用 reject 函數,最好對這個 await 語句進行錯誤處理,具體方式參照 async 函數基本用法中關於錯誤處理的內容。

針對第二點,若是兩個或多個 await 命令後的異步操做沒有依賴關係,執行時,需先觸發第一個,等待異步完成,再觸發第二個,再等異步完成,依次類推,這樣比較耗時,性能很差,因此應該將這些異步操做同時觸發,觸發方式參照 async 函數基本用法中的 await 異步併發的內容。

針對第三點,若是聲明一個 async 函數並傳入一個數組,數組裏面存儲的都是 Promise 實例,若使用 forEach 循環數組,因爲函數的執行上/下文發生了變化,此時使用 await 命令會報錯。

// 循環內使用 await
// 建立 Promise 實例
let p1 = Promise.resolve("p1 success");
let p2 = Promise.resolve("p2 success");
let p3 = Promise.resolve("p3 success");


// async 函數
async function fn(promises) {
    promise.forEach(function (promise) {
        await promise;
    });
}

fn([p1, p2, p3]); // 執行時報錯


// 修改方式
async function fn(promises) {
    for(let i = 0; i < promises.length; i++) {
        await pormises[i];
    }
}

fn([p1, p2, p3]); // 正常執行

<hr/>

總結

async/await 的實現原理,其實就是在 async 函數內部邏輯映射成了 Generator 函數並集成了一個相似於 co 的執行器,因此咱們使用 async/await 的時候,代碼更簡潔,沒有了本身觸發遍歷器的 next 或調用 co 充當執行器的過程,只須要關心 async 函數的內部邏輯就能夠了,由於寫法與同步相同,更提升了代碼的可讀性,因此說 async/await 是異步編程的終極大招。

因爲 async/await 是 ES7 規範,在瀏覽器端的支持並非那麼的友好,因此如今這種寫法多用在 NodeJS 的異步操做當中,在 NodeJS 框架 Koa 2.x 版本獲得普遍應用。

最後但願你們在讀過異步發展流程這個系列以後,對 JavaScript 異步已經有了較深的認識,並能夠在不一樣狀況下游刃有餘的使用這些處理異步的編程手段。

相關文章
相關標籤/搜索