JavaScript ES7的async/await語法讓異步promise操做起來更方便。若是你須要從多個數據庫或者接口按順序異步獲取數據,你可能最終寫出一坨糾纏不清的promise與回調。然而使用async/await可讓咱們用更加可讀、可維護的方式來表達這種邏輯。java
這篇教程以圖表與簡單例子來闡述JS async/await的語法與運行機理。數據庫
在深刻以前,咱們先簡單回顧一下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組合起來。寫許多的then語句以及匿名函數很容易失控。
好比,咱們須要實現如下邏輯:
下面的代碼演示如何達到這個要求:
// 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函數是定義會返回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'; }
之前,當咱們產生一個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/