在常規的服務器端程序設計中, 比如說爬蟲程序, 發送http請求的過程會使整個執行過程阻塞,直到http請求響應完成代碼才會繼續執行, 以php為例子
$url = "http://www.google.com.hk"; $result = file_get_contents($url); echo $result;
當代碼執行到第二行時,程序便陷入了等待,直到請求完成,程序才會繼續往下跑將抓取到的html輸出。這種做法的好處是代碼簡潔明了,運行流程清晰, 容易維護。 缺點就是程序的運行速度依賴於http請求的響應時間,影響程序的運行效率。 然而, 因為web程序本身特質的原因,這種問題是避無可避的,程序依賴於http響應的結果和保證自身的迅速響應兩者之間是存在矛盾的, 肯定無法兼顧。
但是在客戶端程序或者非http應用的場景下是不存在類似的沖突的, 在Java或C#客戶端編程中,碰到這種問題一般都是開啟兩個線程各干各的。而在JavaScript中,因為語言本身不支持多線程, 所以此類問題是使用回調函數來解決。
以最簡單的前端ajax請求為例
$.get("data.json", function ( response ) { console.log("2"); }); console.log("1")
代碼先輸出1,再輸出2,整個程序執行流程並未因http請求而被阻塞,回調函數方案完美的把問題解決。 然而,這只是最簡單回調函數示例,假如回調函數嵌套了許多層呢?
$.get("data1.json", function (response) { $.get(response.url, function (response) { $.get(response.url, function (response) { console.log(response); }); }); });
回調嵌套的越深,代碼運行邏輯就越難理清楚, 如果在上面代碼的基礎上再混入一些復雜的業務邏輯,那代碼將會極難維護, 到時候遇到問題了剪不斷理還亂的感覺肯定會讓人紅着眼睛罵娘。 雖然這種回調嵌套的場景在web前端開發中比較罕見, 但在nodejs服務器端開發領域還是常見的。 那如何克服這個問題?假如用php來寫, 那便是一件很輕松的事了。
$response = file_get_contents("data1.json"); $response1 = file_get_contents($response["url"]); $response2 = file_get_contents($response1["url"]); echo $response;
以php發送http請求的方案來實現, 代碼邏輯就清晰了許多。 在古時候 ,JavaScript想以這種方式實現ajax那就是痴人說夢,但是當JavaScript升級至es6版本后,通過特定的途徑也可實現這種寫法。 在網上這種寫法被稱之為“以同步的方式編寫異步代碼”,但是我覺得這種說法容易把人給搞迷糊,可以直接把這種寫法稱之為:“同步寫法”, 因為里面的異步執行已經被隱藏了起來。 要實現這種寫法必須使用async和await這兩個關鍵字。在兩個關鍵字是es7的范疇, es6還不支持,但是可以通過特定的工具將使用這兩個關鍵字的代碼轉為es6的代碼去執行, 比如說TypeScript和babel, 在此文中使用的代碼示例都是由TypeScript實現。對於async和await的底層機制這里就不詳述了, 以免將文章的篇幅拖的很長,這里就講解一下這兩個關鍵字能實現的效果。 先把上面用JavaScript實現的多層嵌套回調用同步的方式來改寫, 代碼如下
async function ajax(url) { return new Promise(function (resolve, reject) { let ajaxSetting = { url: url, success: function (response) { resolve(response); }, error: function () { reject("請求失敗"); } } $.ajax(ajaxSetting); }); } async function run() { let response1 = await ajax("data1.json"); let response2 = await ajax(response1["url"]); let response3 = await ajax(response2["url"]); console.log(response3); } //不阻塞 run();
代碼由ajax和run這兩個函數組成, ajax是對jquery ajax的封裝,使之能不使用回調函數就能獲得ajax的響應結果。 當函數被聲明為async類型時,如果這個函數要有返回值 ,並且返回值要在某個回調函數中獲得,那么這個函數的返回結果就只能是一個 Promise對象,就像示例的ajax函數一樣,返回值如果是其它類型那就達不到期望的效果。 Promise構造函數的參數是一個函數,resolve和reject分別是這個函數的兩個參數,同時這兩個參數自身也是函數類型,這兩個參數有着重要的意義,在這里它們的作用就是將ajax的響應內容給返回出去,resolve表示返回正常狀況下的值, reject表示返回異常狀態下的值。按照傳統的編碼方式, 可以將reject看作是拋出了一個異常,像throw "請求失敗", 這樣,在函數調用的外部可以用try catch進行捕獲。將值傳出去為什么要通過這兩個參數呢?因為沒轍啊, 試想一下,ajax的回調函數中使用return語句, 意義何在?因此也只能變向的通過Promise將返回值扔給外部的調用者。 所以,使用async和await的第一個要點就是
當函數要獲得異步結果時,可以函數聲明為async類型, 函數的返回值設為Promise類型對象,而Promise中的resolve和reject是用來向async函數返回結果的, 功效如同普通函數的return語句。
async類型函數要怎么使用呢?有兩種方法,一種是直接調用, 直接調用的話函數前面async關鍵字就被忽略了, 調用函數返回的結果就是一個Promise對象, Promise對像如何使用在這里不進行深究,大致就是像下面這樣的寫法
ajax("data1.json").then(function( response ){ …… });
還是以回調函數的形式出現,改進代碼所帶來的意義並沒有體現。
另一種方法是在調用函數時加上await關鍵字,await的意義就在於接收async函數中的Promise對象中resolve和reject傳遞的值 ,而且除非resolve和reject這兩個函數在回調函數中被調用到了, 否則代碼就一直被阻塞在那里?換句話說, resolve和reject的調用是用來通知await等待結束,代碼可以繼續執行了。 這種寫法不就是之前想方設法想實現的同步寫法么?跟php的寫法區別在於多了 await、async、Promise這三個概念, 但是在不考慮其中的內部運行原理的話, 代碼的執行流程上已經和同步的寫法沒一絲區別了。有一點需要注意, 假如需要在函數中使用await調用,那么這個函數也必須被聲明為async類型, 否則編譯出錯, 程序無法正常運行。 所以, 第二個要點就是
await就是用來等待Promise對象中resolve和reject這兩個函數的執行的,並且將這兩個函數傳遞的參數當作返回結果賦給變量,如同run函數中的代碼示例那樣。 別外, await必須被夾在兩個async中間, 一個是await調用的函數,一個是await所在的函數。
至於Promise中的reject,就是用來拋異常的, 在外await調用之外可使用try catch捕獲,代碼如下
async function run() { try { let response1 = await ajax("data1.json"); let response2 = await ajax(response1["url"]); let response3 = await ajax(response2["url"]); console.log(response3); } catch (ex) { console.log(ex); } }
此文只是純粹的講解 await和async能起什么樣的作用?如何使用?至於深入細節方面的知識, 有興趣的同學可以去阮一峰的博客里學習, 附上鏈接地址
http://www.ruanyifeng.com/blog/2015/05/async.html