1. 程式人生 > 實用技巧 >圖解 Await 和 Async

圖解 Await 和 Async

原文連結:Await and Async Explained with Diagrams and Examples

文章目錄

  1. 簡介
  2. Promise
  3. 問題:組合 Promise
  4. Async 函式
  5. Await
  6. 錯誤處理
  7. 討論

簡介

JavaScript ES7中的 async/await 使得協調非同步 promise 變得更容易。如果你需要從多個數據庫或 API 非同步獲取資料,則可以使用 promise 和回撥函式。async / await 使我們更簡潔地表達這種邏輯,並完成更易讀和可維護的程式碼。 本教程將使用圖表和簡單示例來解釋 JavaScript中 的 async / await 語法。
在講解之前,我們從 promises 的簡要概述開始。如果你已經瞭解了 JS 中的 promise,請隨時跳過本節。

Promises

在 JavaScript 中,promise 代表非阻塞非同步執行的抽象物件。JS中的 promise 與 Java 中的Future 或C#的<a href="https://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx"> Task 類似,如果你瞭解它們的話那就很容易理解。 Promise 通常用於網路和 I/O 操作,例如讀取檔案或者發出 HTTP 請求。我們可以產生一個非同步 promise ,並使用 then 的方法來附加一個回撥函式,這個回撥函式當 promise 完成時將會被觸發,這種方法不會阻止當前的“執行緒”執行。回撥函式本身可以返回 promise,使我們可以有效地連結 promises
為了容易理解,在所有示例中,我們假設 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/'); // Line 3
promise.then(result => console.log(result)); // Line 4

console.log("Can't know if promise has finished yet...");  
我們在第3行產生了一個新的 Promise,然後在第4行新加一個回撥函式。因為promise 是非同步的,所以當我們到達第6行時,我們不知道 promise 是否已經完成。如果我們多次執行程式碼,我們可能會每次得到不同的結果。換句話說,任何 promise 之後的程式碼都是與 promise 同時執行的。 promise 完成之前,並沒有辦法阻止當前的操作順序。 這與 Java 中的 Future.get 不同,其允許我們阻止當前執行緒,然後之後完成。在 JavaScript 中,我們不能等待 promise。在 promise 之後排程程式碼的唯一方法是通過 then 附加回調函式。 下圖描繪了該示例的計算過程:
promise 的計算過程。呼叫“執行緒”不能等待 promise 。在 promise 之後排程程式碼的唯一方法是通過then方法指定回撥函式。
當 promise 成功返回時,只有通過then方法指定回撥函式才能執行。如果它失敗了(例如由於網路錯誤),回撥函式將不會執行。為了處理失敗的 promise ,你可以通過catch附加另一個回撥函式:
rp('http://example.com/').
  then(() => console.log('Success')).
  catch(e => console.log(`Failed: ${e}`))  
最後,為了測試一下,我們可以使用Promise.resolvePromise.reject方法建立成功或失敗的“虛擬”promises:
const success = Promise.resolve('Resolved');
// 將會顯示 "Successful result: Resolved"
success.
  then(result => console.log(`Successful result: ${result}`)).
  catch(e => console.log(`Failed with: ${e}`))

const fail = Promise.reject('Err');
// 將會顯示 "Failed with: Err"
fail.
  then(result => console.log(`Successful result: ${result}`)).
  catch(e => console.log(`Failed with: ${e}`))  
有關 promises 的更詳細的教程,請檢視這篇文章

組合 Promise

使用單一 Promise 是簡單有效的。但是,當我們需要對複雜的非同步邏輯進行程式設計時,我們可能會以組合多個 Promise。編寫所有的子句和匿名回撥可能很容易失控。 例如,假設我們需要編寫一個程式:
  1. 進行HTTP請求,等待完成,列印結果;
  2. 然後進行其他兩個並行HTTP呼叫;
  3. 當它們都完成時,列印結果。 以下程式碼段演示瞭如何完成此操作:
    //進行第一個呼叫
    const call1Promise = rp('http://example.com/'); // Line 2
    
    call1Promise.then(result1 => {
      //在第一個請求完成後執行
      console.log(result1);
    
    	const call2Promise = rp('http://example.com/'); // Line 8
    	const call3Promise = rp('http://example.com/'); // Line 9
    
      return Promise.all([call2Promise, call3Promise]); // Lin 11
    }).then(arr => { // Line 12
      //兩個 promise 完成後執行
      console.log(arr[0]);
      console.log(arr[1]);
    }) 
我們首先進行第一個 HTTP 呼叫,並使用回撥以在其完成時執行(第1-3行)。在回撥中,我們為後續的 HTTP 請求產生了兩個 Promise(第8-9行)。這兩個 Promise 同時執行並且我們需要安排一個回撥,當它們都完成時。因此,當它們都執行完成時,我們需要通過Promise.all(第11行)將它們組合成一個單一的 Promise。回撥的結果是一個 Promise,我們需要l連線另一個回撥函式來列印結果(行12-16)。 下圖描述了計算流程:
計算過程中的 Promise 組合。我們使用“Promise.all”將兩個併發的 Promise組合成一個 Promise。
對於這樣一個簡單的例子,我們最終得到了 2 個回撥,並且必須使用Promise.all來同步併發Promise。如果我們不得不再執行一些非同步操作或新增錯誤處理怎麼辦?這種方法最終很容易崩潰於then-s,Promise.all呼叫和回撥三者混雜在一起。

Async

非同步函式是用於定義返回Promise的函式的快捷方式。 例如,以下定義是等價的:
function f() {
  return Promise.resolve('TEST');
}

// asyncF相當於f!
async function asyncF() {
  return 'TEST';
}  
類似地,丟擲異常的非同步函式等效於返回拒絕 Promise 的函式:
function f() {
  return Promise.reject('Error');
}

// asyncF相當於f!
async function asyncF() {
  throw 'Error';
}  

Await

當我們使用 promise 之後,我們只能通過then來傳回回撥函式(callback)。 不允許直接等待一個 promise 執行完畢是為了鼓勵使用者書寫�非阻塞的程式碼,不然使用者會更樂意寫阻塞的程式碼,因為它比 promise 和回撥函式更簡單。 然而,為了同步 promise, 我們需要允許 promise 之間相互等待。換句話說,如果一個非同步的操作(例如封裝在一個 promise 中)就應該去等待另一個非同步的操作去完成。但是 JavaScript 直譯器如何判斷一個操作是否在 promise 中執行呢? 答案就是 async 關鍵字。每一個 async 函式都會返回一個 promise。也就是說, JavaScript 直譯器就會把所有在 aysnc 函式中的操作封裝到 promise 中並非同步執行。這樣就可以讓它們去等待其他的 promise 完成。 按下 await 關鍵字,await 只能在 async 函式中使用,作用是讓我們同步的等待另一個 promise 執行完畢。如果在 async 函式之外使用 promise 的話,依舊需要使用 then 回撥函式:
async function f() {
  // 返回值將作為 promise 被處理(resolve)之後的結果
  const response = await rp('http://example.com/');
  console.log(response);
}
// 不能在 async 函式之外使用 await 關鍵字
// 需要使用 then 回撥
f().then(() => console.log('Finished'));  
現在,來看看如何解決剛在在上面一節出現的問題:
// 將解決問題的方法封裝到一個非同步的函式中
async function solution() {
 // 等待第一個 HTTP 呼叫並且打印出結果
  console.log(await rp('http://example.com/'));

  // 生成 HTTP 呼叫但是不等待它們執行完畢 - 同時執行
  const call2Promise = rp('http://example.com/'); // 不等待! // Line 7
  const call3Promise = rp('http://example.com/'); // 不等待! // Line 8

  //在它們都被呼叫之後 - 等待它們執行完畢
  const response2 = await call2Promise; // Line 11
  const response3 = await call3Promise; // Line 12

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

// 呼叫 async 函式
solution().then(() => console.log('Finished'))  
在上面的程式碼段中,我們將解決方案封裝到了一個 async 函式中,這樣我們就可以直接的等待(await) promise 執行完畢。這樣避免了使用 then 回撥函式。 最後,我們呼叫了 async 函式,這個函式只是簡單的生成了一個封裝呼叫其他 promisepromise 在第一個例子(沒有 asyncawait)中,那些 promise 會並行啟動。這種情況下我們進行了同樣的操作(第7,8行)。注意,直到11到12行,我們都沒有使用 await。當所有的promise都執行完畢(resolve),我們才去阻塞程式的執行。之後,我們知道兩個 promise 都執行完畢了(就像在之前的例子中,使用 Promise.all(...).then(...) 一樣)。 在底層的計算過程上,這個過程和先前章節所述的過程是相同的,但是程式碼更加直觀,可讀性更好。 在引擎中,async/await 實際上轉成了 promise 和 then傳入的回撥函式。換句話說,它是 promise 的語法糖。每次我們使用 await,直譯器就會生成一個 promise,然後把其餘的操作從 async 函式取出來放到 then 傳入的回撥函式中。 考慮一下下面的例子:
async function f() {
  console.log('Starting F');
  const result = await rp('http://example.com/');
  console.log(result);
}  
函式f在底層計算過程描述如下圖。由於f是非同步的,它將和呼叫者同步執行: 函式f開始執行並且生成了一個promise。同時,函式其餘的部分被封裝到回撥函式中,安排在promise執行完畢之後再執行。

錯誤處理

在前面幾個例子中,我們假設promise 成功的解決(reslove).於是,等待一個promise返回結果。事實上,如果等待的promise失敗(reject)了,那麼async函式將會返回一個異常(exception)。我們可以使用標準的try/catch去處理這種情況:
async function f() {
  try{
    const promiseResult = await Promise.reject('Error');
  } catch (e) {
    console.log(e);
  }
}  
如果一個async函式沒有處理異常,不管它是一個被拒絕(reject)的promise還是其他的 bug 造成的,它將返回一個被拒絕(reject)的promise:
async function f() {
  // 丟擲異常
  const promiseResult = await Promise.reject('Error');
}

// 將列印 "Error"
f().
  then(() => console.log('Success')).
  catch(err => console.log(err))

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

// 將列印 “Error”
g().
  then(() => console.log('Success')).
  catch(err => console.log(err))  
使用已知的異常處理機制將使我們方便的處理被拒絕(reject)的promise

討論

Async/awaitpromises的一種補充語言結構。它允許我們使用較少樣板的 promise。然而async/await不能取代純粹 promise 的需要。例如,如果從一個普通的函式或者全域性範圍內呼叫一個async函式,我們無法使用await,我們將藉助於普通的promises(譯者注:原文使用的是vanilla promise):
async function fAsync() {
  // 事實上返回值是 Promise.resolve(5)
  return 5;
}
// 不能呼叫 await fAsync(), 需要使用 then/catch
fAsync().then(r => console.log(`result is ${r}`));  
我通常嘗試將我大部分的非同步邏輯封裝到一個或者幾個 async 函式中,然後從非非同步的程式碼中呼叫。這極大地減少了我編寫then/catch回撥的數量。 async/await 結構是更簡潔處理 promise 的語法糖。每一個 async/await 結構都可以使用純粹的 promise 重寫。最終,這是一個風格和簡潔方面的問題。 學者們指出併發( concurrency )和並行( parallelism )有區別。檢視Rob Pike關於該主題或我之前的帖子。併發是關於組合獨立程序(在過程的一般含義中)一起工作,而並行是關於實際上同時執行多個程序。併發是關於應用程式的設計和結構,而並行性就是實際的執行。 以多執行緒應用程式為例。將應用程式分隔為執行緒定義其併發模型。這些執行緒在可用核心上的對映定義了其級別或並行。併發系統可以在單個處理器上有效執行,在這種情況下,它不是並行的。 在這種情況下,promise允許我們將程式分解為可並行執行的併發模組。實際的 JavaScript執行是否並行取決於 JavaScript 直譯器實現。例如,Node.js是單執行緒的,如果 promise 是 CPU 繫結的,那麼並不會看到很多並行程序。然而,如果您通過類似 Nashorn 的工具將程式碼編譯成 java 位元組碼,理論上你可以在不同的 CPU 核心上對映CPU繫結的 promise 並實現並行執行。因此,在我看來,promise(普通或通過async/await)構成了JavaScript應用程式的併發模型。 文章轉自 | Github 文章連結 | 圖解 Await 和 Async