ES7中,async/await
語法使異步promise
的協調變得很簡單。若是你須要以特定順序異步獲取來自多個數據庫或API的數據,可使用雜亂的promise或回調函數。async/await
使咱們能夠更簡便地處理這種邏輯,代碼的可讀性和可維護性也更好。java
在該教程中,咱們用圖表和一些簡單的例子來解釋async/await
的語法和語義。
開始講解以前,咱們先對promise
進行一個簡單的概述,若是你對promise已經很熟悉了,能夠跳過該部份內容。node
在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.resolve
和Promise.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來處理。大量的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行)。在回調函數中,咱們又相繼發起兩個HTTP請求生成了兩個promise。這兩個promise並行運行;當他們都執行完後,咱們還須要爲其綁定一個回調函數。所以,咱們用promise.all
將這兩個promise組合成一個promise, 只有當他們都完成後,這個promise纔會完成。因爲第一個回調函數的結果是promise,所以咱們鏈式地調用另外一個then方法和回調函數輸出最終結果。
下圖描述了這個執行過程:
對於這麼簡單的例子,咱們就用了兩個then
回調和promise.all
來同步並行的promise。試想若是咱們執行更多的異步操做或者增長錯誤處理函數呢?這種方式很容易讓代碼變成一堆雜亂的then
、promise.all
和回調函數。
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'; }
咱們不能同步等待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應用程序的併發模型。