以圖表和示例的角度解讀async/await

介紹

ES7中,async/await 語法使異步promise的協調變得很簡單。若是你須要以特定順序異步獲取來自多個數據庫或API的數據,可使用雜亂的promise或回調函數。async/await使咱們能夠更簡便地處理這種邏輯,代碼的可讀性和可維護性也更好。java

在該教程中,咱們用圖表和一些簡單的例子來解釋async/await的語法和語義。
開始講解以前,咱們先對promise進行一個簡單的概述,若是你對promise已經很熟悉了,能夠跳過該部份內容。node

Promises

在js中,promise表示抽象的非阻塞異步執行。js中的promise與Java中的 Future或C#中的Task很類似。數據庫

promise一般用於網絡和I/O操做-例如,讀取文件,發起HTTP請求。爲了避免阻塞當前執行線程,咱們建立一個異步promise,使用then方法綁定一個回調函數,該回調函數會在promise完成後觸發。回調函數自己也能夠返回一個promise,因此promise能夠高效的鏈式調用。編程

簡單起見,全部的例子中咱們都假定request-promise庫已經安裝和加載完成了,以下所示:promise

var rp = require('request-promise');

如今咱們能夠像這樣發起一個簡單的HTTP GET請求,該方法返回一個promise:網絡

const promise = rp('http://example.com/')

接下來,看一個例子:多線程

console.log('Starting Execution');

const promise = rp('http://example.com/');
promise.then(result => console.log(result));

console.log("Can't know if promise has finished yet...");

在第三行,咱們建立了一個promise,而後咱們在第四行中爲其綁定了一個回調函數。因爲promise是異步執行的
,因此執行到第六行時,咱們不肯定promise有沒有完成。屢次運行上面的代碼,獲得的結果可能每次都不同。更通俗地講,promise後面的代碼和promise是並行運行的。併發

在promise完成以前,沒有辦法中斷當前的操做序列。這與Java中的 Future.get是不一樣的,Future.get容許咱們中斷當前的線程直到Future完成。js中,咱們不會輕易地等待promise執行完成。在promise完成以後安排代碼的惟一方式是經過then方法綁定回調函數。異步

下圖描述了該示例的計算過程:
圖片描述async

then方法中綁定的回調函數只有當promise成功的時候纔會調用。若是promise失敗的話(例如,因爲網絡錯誤),回調不會執行。爲了處理失敗的promise,須要經過catch綁定另外一個回調函數。

rp('http://example.com/').
    then(() => console.log('Success')).
    catch(e => console.log(`Failed: ${e}`))

最後,爲了測試一下效果,咱們經過Promise.resolvePromise.reject簡單地生成成功和失敗的promise:

const success = Promise.resolve('Resolved');
// Will print "Successful result: Resolved"
success.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))


const fail = Promise.reject('Err');
// Will print "Failed with: Err"
fail.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))

有關promise更詳細的教程,查看這篇文章

問題-組合Promise

單個promise是很簡單的。但是,咱們編寫複雜的異步邏輯時,可能須要組合使用多個promise來處理。大量的then語句和匿名回調函數很容易讓代碼變得不可維護。

例如,咱們要編寫一個以下功能的代碼:

  1. 發起一個HTTP請求,等待完成後,打印出結果

  2. 而後發起兩個並行的HTTP請求;

  3. 後兩個請求都完成後,打印出他們的結果。

下面的代碼片斷演示了上述功能的實現:

// Make the first call
const call1Promise = rp('http://example.com/');

call1Promise.then(result1 => {
    // Executes after the first request has finished
    console.log(result1);

    const call2Promise = rp('http://example.com/');
    const call3Promise = rp('http://example.com/');

    return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
    // Executes after both promises have finished
    console.log(arr[0]);
    console.log(arr[1]);
})

首先發起第一個HTTP請求,當該請求完成後,調用它的回調函數(1-3行)。在回調函數中,咱們又相繼發起兩個HTTP請求生成了兩個promise。這兩個promise並行運行;當他們都執行完後,咱們還須要爲其綁定一個回調函數。所以,咱們用promise.all將這兩個promise組合成一個promise, 只有當他們都完成後,這個promise纔會完成。因爲第一個回調函數的結果是promise,所以咱們鏈式地調用另外一個then方法和回調函數輸出最終結果。

下圖描述了這個執行過程:

圖片描述

對於這麼簡單的例子,咱們就用了兩個then回調和promise.all來同步並行的promise。試想若是咱們執行更多的異步操做或者增長錯誤處理函數呢?這種方式很容易讓代碼變成一堆雜亂的thenpromise.all和回調函數。

Async 函數

async 函數提供了一種簡潔的方式來定義一個返回promise的函數。
例如,下面兩種定義是等價的:

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

類似地,在異步函數拋出異常與返回一個reject promise對象的函數等價:

function f() {
    return Promise.reject('Error');
}

// asyncF is equivalent to f!
async function asyncF() {
    throw 'Error';
}

Await

咱們不能同步等待promise的完成。只能經過then方法傳入一個回調函數。咱們鼓勵非阻塞編程,所以同步等待promise是不容許的。不然,開發者會產生編寫同步腳本的想法,畢竟同步編程要簡單的多。

可是,爲了同步promise咱們須要容許他們等待彼此的完成。換句話說,若是操做是異步的(也就是說包裹在promise中),它應該能夠等待其餘異步操做的完成。可是,js解析器怎麼知道操做是否跑在promise中?

答案是async關鍵字。每一個async函數返回一個promise。所以,js解析器知道全部的操做都位於async函數中,並將全部的代碼包裹在promise中異步地執行。因此,async函數,容許操做等待其餘promise的完成。

說一下await關鍵字。它只能用在async函數中,容許咱們同步等待promise的完成。若是在async函數外邊使用promise,咱們仍然須要使用then回調函數。

async function f(){
    // response will evaluate as the resolved value of the promise
    const response = await rp('http://example.com/');
    console.log(response);
}

// We can't use await outside of async function.
// We need to use then callbacks ....
f().then(() => console.log('Finished'));

如今咱們看一下前面的那個例子如何用async/await進行改寫:

/ Encapsulate the solution in an async function
async function solution() {
    // Wait for the first HTTP call and print the result
    console.log(await rp('http://example.com/'));

    // Spawn the HTTP calls without waiting for them - run them concurrently
    const call2Promise = rp('http://example.com/');  // Does not wait!
    const call3Promise = rp('http://example.com/');  // Does not wait!

    // After they are both spawn - wait for both of them
    const response2 = await call2Promise;
    const response3 = await call3Promise;

    console.log(response2);
    console.log(response3);
}

// Call the async function
solution().then(() => console.log('Finished'));

以上代碼,咱們的解決方案就封裝在了async函數中。咱們能夠直接await promise的執行,省掉了then回調函數。最後,咱們只須要調用async函數。它封裝了調用其餘promise的邏輯,並返回一個promise。

實際上在上面的例子中,promise是並行觸發的。本例中也同樣(7-8行)。注意第12-13行咱們使用了await阻塞主線程,等待全部的promise執行完成。後面,咱們看到promise都完成了,和前面的例子相似(promise.all(...).then(...))。

其執行流程與前例的流程是相等的。可是,代碼變得更具可讀性和簡潔。

底層實現上,await/async實際上轉換成了promise,換句話說,await/async是promise的語法糖。每次咱們使用await時,js解析器會生成一個promise,並將async函數中的剩餘代碼放到then回調中去執行。
思考下面的例子:

async function f() {
    console.log('Starting F');
    const result = await rp('http://example.com/');
    console.log(result);
}

下面描述函數f的基本計算過程。因爲f是異步的,它會與調用方並行執行:

圖片描述
函數f開始執行,遇到await後生成一個promise。此時,函數的其他部分被封裝在回調中,並在promise完成後執行。

錯誤處理

前面的大部分例子中,咱們都是假設promise成功完成了。所以,等待promise返回一個值。若是咱們等待的promise失敗了,在async函數中會致使一個異常。咱們可使用標準的try/catch來捕獲和處理它。

async function f() {
    try {
        const promiseResult = await Promise.reject('Error');
    } catch (e){
        console.log(e);
    }
}

若是async函數沒有處理異常,不論是promise reject了,仍是產生了其餘bug,它都會返回一個rejected的promise對象。

async function f() {
    // Throws an exception
    const promiseResult = await Promise.reject('Error');
}

// Will print "Error"
f().
    then(() => console.log('Success')).
    catch(err => console.log(err))

async function g() {
    throw "Error";
}

// Will print "Error"
g().
    then(() => console.log('Success')).
    catch(err => console.log(err))

這給咱們提供了一種簡便的方法,經過已知的異常處理機制來處理被rejected的promise。

討論

async/await 在語言結構上是對promise的補充。可是,async/await 並不能取代純promise的需求。例如,在正常函數和全局做用域咱們不能使用await,因此須要使用普通的promise:

async function fAsync() {
    // actual return value is Promise.resolve(5)
    return 5;
}

// can't call "await fAsync()". Need to use then/catch
fAsync().then(r => console.log(`result is ${r}`));

我一般會將異步邏輯封裝到一個或者少數幾個async函數中,而後在非異步代碼中調用async函數。這樣我能夠最小化下降書寫then/catch的數量。
學者們指出,併發性和並行性是有區別的。併發性是指將獨立的進程(通常意義上的進程)組合在一塊兒,而並行其實是同時執行多個進程。併發性是關於應用程序設計和結構的,而並行性是關於實際執行的。

咱們以一個多線程應用程序爲例。應用程序分離到線程定義了它的併發模型。這些線程在可用內核上的映射定義了它的級別或並行性。併發系統能夠在單個處理器上高效運行,在這種狀況下,它不是並行的。

圖片描述

就此而言,promise容許咱們將一個程序分解爲並行的併發模塊,也能夠不併行運行。實際的JavaScript執行是否並行取決於實現。例如,Node Js是單線程的,若是一個promise是CPU綁定的,你就不會看到太多的並行性。然而,若是你經過像Nashorn這樣的東西把你的代碼編譯成java字節碼,理論上你可能可以在不一樣核心上映射CPU綁定的promise,而且實現並行性。所以,在我看來,promise(不管是普通的或經過await/async)構成了JavaScript應用程序的併發模型。

相關文章
相關標籤/搜索