使用圖解和例子解釋Await和Async

簡介

JavaScript ES7中的 async/await 語法使得異步Promise變得更加容易。 若是您須要以某種順序從多個數據庫或API異步獲取數據,則可使用promise和回調構成的麪條式的代碼。 async/await 構造容許咱們更簡潔地表達這種邏輯且代碼更易讀和可維護。數據庫

本教程將使用圖表和簡單示例來解釋JavaScriptasync/await 語法和語義。編程

在咱們開始以前,讓咱們從一個Promise的簡要概述開始。 若是您已經瞭解了JSPromise,請隨時跳過本節。promise

Promises

在JavaScript中,Promises表明非阻塞異步執行的抽象。 若是瞭解其餘語言的話,JSPromise與Java的Future或C#的Task相似。微信

Promises一般用於網絡和I/O操做 - 例如從文件讀取或發出HTTP請求。 若是不須要阻塞當前的「線程」執行,咱們能夠產生一個異步Promises,並使用then方法來傳入一個回調函數,它在promise完成時將被觸發。 回調函數自己能夠返回Promise,所以咱們能夠鏈式調用Promise網絡

爲了簡單起見,在全部示例中,咱們假設request-promise已經安裝並能夠像下面這樣子加載:併發

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

如今咱們能夠作一個簡單的HTTP GET請求,返回一個Promise異步

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

如今,咱們來看一個例子:async

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同時運行的。ide

Promise完成以前,咱們沒有任何合理的理由阻止當前的操做順序。 這與Java的Future.get不一樣,它容許咱們阻止當前線程,直到未來完成。 在JavaScript中,咱們不能等待Promise完成。 在Promise完成以後執行代碼的惟一方法是經過then方法傳入回調函數。函數

下圖描繪了該示例的計算過程:

clipboard.png

Promise的計算過程。 調用「線程」不能等待Promise。 在Promise以後執行代碼的惟一方法是經過then方法指定回調函數。

只有當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和匿名回調能夠很容易失去對代碼的控制。

例如,假設咱們須要編寫一個程序:

  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/');

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

咱們首先發起了第一個HTTP請求,並在其完成時運行回調函數(第1-3行)。 在回調中,咱們爲後續的HTTP請求產生了兩個Promise(第8-9行)。 這兩個Promise同時運行,咱們須要安排一個回調,在它們都完成時調用。 所以,咱們須要經過Promise.all(第11行)將它們組合成一個單一的Promise,當它們完成時,它們就能夠正確調用。 而後咱們傳入了另外一個打印結果的回調(第14-15行)。

下圖描述了計算流程:

clipboard.png

對於這樣一個簡單的例子,咱們最終獲得了2個嵌套的回調函數,而且必須使用Promise.all來同步併發Promise。 若是咱們不得再也不運行一些異步操做或添加錯誤處理怎麼辦? 這種方法能夠很容易地改寫成用Promise.all和多個then鏈接起來的鏈式麪條代碼。

咱們能夠重寫上面的例子來使用「promise chaining」,以下所示:

// 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]);
})

這樣可讀性更高一些,儘管咱們仍然須要連接兩個回調函數並使用Promise.all

Async 函數

Async函數是返回Promise函數的簡寫。

例如,如下定義是等價的:

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

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

相似地,拋出異常的Async函數等效於返回reject Promise的函數:

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

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

Await

當咱們產生承諾時,咱們沒法同步等待完成。 咱們只能經過一個回調。 不容許等待承諾鼓勵開發非阻塞代碼。 不然,開發人員將被誘惑執行封鎖操做,由於它比使用承諾和回調更容易。

當咱們建立Promise時,咱們沒法同步等待完成。 咱們只能經過一個回調。 不容許等待Promise,鼓勵開發非阻塞代碼。 不然,開發人員將更容易使用鎖定當前線程的操做,由於它比使用Promise和回調更容易。

然而,爲了同步Promise,咱們須要容許他們相互等待。 換句話說,若是操做是異步的(即封裝在Promise中),則應該可以等待另外一個異步操做完成。 可是JavaScript解釋器如何知道一個操做是否在Promise中運行?

答案是在async關鍵字。 每一個async函數都返回一個Promise。 所以,JavaScript解釋器知道async函數中的全部操做都將被封裝在Promise中並異步運行。 因此可讓他們等待其餘的Promise完成以後再繼續執行。

當咱們使用await關鍵字。 它只能用於async功能,並容許咱們同步等待Promise。 若是咱們在async函數以外使用Promise,咱們仍然須要使用回調函數:

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,從而避免了回調的須要。 最後,咱們調用async函數,該函數只是產生一個封裝了調用其餘Promise的邏輯的Promise

事實上,在第一個例子中(沒有async/await),這些Promise將會並行開始。 在這種狀況下,咱們作一樣的(7-8行)。 請注意,直到第11-12行,當咱們使用了await,直到兩個Promise都已經完成爲止。 以後,咱們知道這兩個Promise都已經完成了(相似於前面的例子中使用Promise.all(...)而後(...))。

實際計算過程等同於上一節所述的過程。 然而,代碼更加可讀和直觀。

在引導下,async/await實際上轉化爲Promise,而後回調。 換句話說,它是使用Promise的語法糖。 每次咱們等待,解釋器產生一個Promise,並將其他的操做從異步功能放在一個回調。

咱們來考慮下面的例子:

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

f函數的基礎計算過程以下所示。 因爲f是異步的,它也將與其調用者並行運行

clipboard.png

函數f啓動併產生Promise。 在那一刻,函數的其他部分被封裝在一個回調函數中,而且在Promise完成以後計劃執行。

錯誤處理

在前面的大多數例子中,咱們假設Promise成功執行了。 所以,等待Promise返回值。 若是咱們等待失敗的Promise,這將致使異步功能中的異常。 咱們可使用標準的try/catch來處理它:

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

若是async函數不處理異常,不管是由拒絕Promise仍是其餘錯誤引發的,都將返回被拒絕的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))

這經過已知的異常處理機制使咱們方便地處理被拒絕的Promise

討論

Async/await是一種對Promise的語言上的補充。 它容許咱們以較少的樣板來使用Promise。 可是,Async/await不能取代純粹Promise的須要。 例如,若是咱們從正常函數或全局範圍調用Async函數,咱們將沒法使用await,並將訴諸於vanillaPromise

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}`));

我一般會嘗試將大多數異步邏輯封裝在一個或幾個異步函數中,這是從非異步代碼中調用的。 這最大限度地減小了我須要編寫的try/catch回調的數量。

Async/await結構是更符合Promise的語法糖。 每一個Async/await結構能夠用簡單的Promise重寫。 因此,這是一個風格和簡潔的問題。

關注個人微信公衆號,更多優質文章定時推送
clipboard.png

翻譯自http://nikgrozev.com/2017/10/...

相關文章
相關標籤/搜索