圖與例解讀Async/Await

JavaScript ES7的async/await語法讓異步promise操做起來更方便。若是你須要從多個數據庫或者接口按順序異步獲取數據,你可能最終寫出一坨糾纏不清的promise與回調。然而使用async/await可讓咱們用更加可讀、可維護的方式來表達這種邏輯。java

這篇教程以圖表與簡單例子來闡述JS async/await的語法與運行機理。數據庫

在深刻以前,咱們先簡單回顧一下promise,若是對這方面概念有自信,大可自行跳過。promise

Promise

在JS的世界裏,一個promise抽象表達一個非阻塞(阻塞指一個任務開始後,要等待該任務執行結果產生以後才繼續執行後續任務)的異步流程,相似於Java的Futrue或者C#的Task。瀏覽器

Promise最典型的使用場景是網絡或其餘I/O操做(如讀取一個文件或者發送一個HTTP請求)。與其阻塞住當前的執行「線程」,咱們能夠產生一個異步的promise,而後用then方法來附加一個回調,用於執行該promise完成以後要作的事情。回調自身也能夠返回一個promise,如此我就能夠將多個promise串聯。babel

爲方便說明,假定後續全部的例子都已經引入了request-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...");

咱們在第3行產生了一個promise,而後在第4行附上了一個回調函數。返回的promise是異步的,因此當執行的第6行的時候,咱們沒法肯定這個promise有沒有完成,屢次執行可能有不一樣的結果(譯者:瀏覽器裏執行多少次,這裏promise都會是未完成狀態)。歸納來講,promise以後的代碼跟promise自身是併發的(譯者:對這句話有異議者參見本文最後一節的併發說明)。異步

並不存在一種方法可讓當前的執行流程阻塞直到promise完成,這一點與Java的Futrue.get相異。JS裏,咱們沒法直接原地等promise完成,惟一能夠用於提早計劃promise完成後的執行邏輯的方式就是經過then附加回調函數。async

下面的圖表描繪了上面代碼例子的執行過程:
這裏寫圖片描述

Promise的執行過程,調用「線程」沒法直接等待promise結果。惟一規劃promise以後邏輯的方法是使用then方法附加一個回調函數。

經過then 附加的回調函數只會在promise成功是被觸發,若是失敗了(好比網絡異常),這個回調不會執行,處理錯誤須要經過catch 方法:

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

最後,爲了方便試驗功能,咱們能夠直接建立一些「假想」的promise,使用Promise.resolve生成會直接成功或失敗的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組合起來。寫許多的then語句以及匿名函數很容易失控。

好比,咱們須要實現如下邏輯:

  • 發起一個HTTP請求,等待結果並將其輸出
  • 再發起兩個併發的HTTP請求
  • 當兩個請求都完成時,一塊兒輸出他們

下面的代碼演示如何達到這個要求:

// 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行)。在回調裏,咱們爲另外兩次請求製造了promise(第8-9行)。這兩個promise併發運行,咱們須要計劃一個在兩個都完成時執行的回調,因而,咱們經過Promise.all(第11行)來說他們合併。這第一個回調的返回值是一個promise,咱們再添加一個then來輸出結果(第12-16行)。

如下圖標描繪這個計算過程:
這裏寫圖片描述

將promise組合的計算過程。使用「Promise.all」將兩個併發的promise合併成一個。

爲了一個簡單的例子,咱們最終寫了兩個then回調以及一個Promise.all來同步兩個併發promise。若是咱們還想再多作幾個異步操做或者添加一些錯誤處理會怎樣?這種實現方案最終很容變爲糾纏成一坨的then、Promise.all以及回調匿名函數。

Async函數

一個async函數是定義會返回promise的函數的簡便寫法。

好比,如下兩個定義是等效的:

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

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

類似地,會拋出錯誤的async函數等效於返回將失敗的promise 的函數:

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

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

Await

之前,當咱們產生一個promise,咱們沒法同步地等待它完成,咱們只能經過then註冊一個回調函數。不容許直接等待一個promise是爲了鼓勵開發者寫非阻塞的代碼,否則開發者會更樂意寫阻塞的代碼,由於這樣比promise和回調簡單。

然而,爲了同步多個promise,咱們須要它們互相等待,換句話說,若是一個操做自己就是異步的(好比,用promise包裝的),它應該具有能力等待另外一個異步操做先完成。可是JS解釋器如何知道一個操做是否是在一個promise裏的?

答案就是async關鍵字,全部的async函數必定會返回一個promise。因此,JS解釋器也能夠確信async函數裏操做是用promise包裝的異步過程。因而也就能夠容許它等待其餘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'));

如今咱們來看看咱們能夠如何解決以前提到的問題:

// 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函數裏。這樣咱們就能夠直接對promise使用await了,也就規避了寫then回調。最後咱們調用這個async函數,而後按照普通的方式使用返回的promise。

要注意的是,在第一個例子裏(沒有async/await),後面兩個promise是併發的。因此咱們在第7-8行也是如此,而後直到11-12行才用await來等待兩個promise都完成。這以後,咱們能夠確信兩個promise都已經完成(與以前Promise.all(...).then(...)相似)。

計算流程跟以前的圖表描繪的同樣,可是代碼變得更加已讀與直白。

事實上,async/await其實會翻譯成promise與then回調(譯者:babel實際上是翻譯成generator語法,再經過相似co的函數運行,co內部運行機制離不開promise)。每次咱們使用await,解釋器會建立一個promise而後把async函數的後續代碼放到then回調裏。

咱們來看看如下的例子:

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

f函數的內在運行過程以下圖所描繪。由於f標記了async,它會與它的調用者「併發」:
這裏寫圖片描述
函數f啓動併產生一個promise。在這一刻,函數剩下的部分都會被封裝到一個回調函數裏,並被計劃在promise完成以後執行。

錯誤處理

在以前的例子裏,咱們大多假定promise會成功,而後await一個promise的返回值。若是咱們等待的promise失敗了,會在async函數裏產生一個異常,咱們可使用標準的try/catch來處理它

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

若是async函數不處理這個異常,不論是這異常是由於promise是被reject了仍是其餘的bug,這個函數都會返回一個被reject掉的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))

這就讓咱們可使用熟悉的方式來處理錯誤。

擴展說明

async/await是一個對promise進行補充的語法部件,它能讓咱們寫更少的重複代碼來使用promise。然而,async/await並不能完全取代普通的promise。好比,若是咱們在一個普通的函數或者全局做用域裏使用一個async函數,咱們沒法使用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回調。

async / await是讓promise用起來更簡潔的語法糖。全部的async / await均可以用普通的promise來實現。全部總結來講,這只是個代碼樣式與簡潔的問題。

學院派的人會指出,併發與並行是有區別的(譯者:因此前文都是說併發,而非並行)。參見Rob Pike的講話或者我以前的博文。併發是組合多個獨立過程來一塊兒工做,並行是多個過程同時執行。併發是體如今應用的結構設計,並行是實際執行的方式。

咱們來看看一個多線程應用的例子。將應用分割成多個線程是該應用併發模型的定義,將這些線程放到可用的cpu核心上執行是確立它的並行。一個併發的系統也能夠在一個單核處理器上正常運行,但這種狀況並非並行。
這裏寫圖片描述
以這種方式理解,promise能夠將一個程序分解成多個併發的模塊,它們或許,也可能並不會並行執行。JS是否並行執行要看解釋器自身的實現。好比,NodeJS是單線程的,若是一個promise裏有大量的CPU操做(非I/O操做),你可能感覺不到太多並行。然而若是你用像nashorn這樣的工具把代碼編譯成java字節碼,理論上你能夠把繁重的CPU操做放到其餘內核上來得到平行效果。因而在個人觀點中,promise(不論是裸的仍是有async/await)只是做用於定義JS應用的併發模型(而非肯定邏輯是否會並行運行)。

關於本文

譯者:@安秦

譯文:https://zhuanlan.zihu.com/p/30500864

做者:@Nikolay

原文:http://nikgoozev.com/2017/10/01/async-await/

相關文章
相關標籤/搜索