JavaScript 中最蛋疼的事情莫過於回撥函式巢狀問題。以往在瀏覽器中,因為與伺服器通訊是一種比較昂貴的操作,因此比較複雜的業務邏輯往往都放在伺服器端,前端 JavaScript 只需要少數幾次 AJAX 請求就可拿到全部資料。
但是到了 webapp 風行的時代,前端業務邏輯越來越複雜,往往幾個 AJAX 請求之間互有依賴,有些請求依賴前面請求的資料,有些請求需要並行進行。還有在類似 node.js 的後端 JavaScript 環境中,因為需要進行大量 IO 操作,問題更加明顯。這個時候使用回撥函式來組織程式碼往往會導致程式碼難以閱讀。
現在比較流行的解決這個問題的方法是使用 Promise,可以將巢狀的回撥函式展平。但是寫程式碼和閱讀依然有額外的負擔。
另外一個方案是使用 ES6 中新增的 generator,因為 generator 的本質是可以將一個函式執行暫停,並儲存上下文,再次呼叫時恢復當時的狀態。co 模組是個不錯的封裝。但是這樣略微有些濫用 generator 特性的感覺。
ES7 中有了更加標準的解決方案,新增了 async/await 兩個關鍵詞。async 可以宣告一個非同步函式,此函式需要返回一個 Promise 物件。await 可以等待一個 Promise 物件 resolve,並拿到結果。
比如下面的例子,以往我們無法在 JavaScript 中使用常見的 sleep 函式,只能使用 setTimeout 來註冊一個回撥函式,在指定的時間之後再執行。有了 async/await 之後,我們就可以這樣實現了:
js
async function sleep(timeout) { return new Promise((resolve, reject) => { setTimeout(function() { resolve(); }, timeout); }); } (async function() { console.log('Do some thing, ' + new Date()); await sleep(3000); console.log('Do other things, ' + new Date()); })();
執行此段程式碼,可以在終端中看到結果:
Do some thing, Mon Feb 23 2015 21:52:11 GMT+0800 (CST)
Do other things, Mon Feb 23 2015 21:52:14 GMT+0800 (CST)
另外 async 函式可以正常的返回結果和丟擲異常。await 函式呼叫即可拿到結果,在外面包上 try/catch 就可以捕獲異常。下面是一個從豆瓣 API 獲取資料的例子:
js
var fetchDoubanApi = function() { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { var response; try { response = JSON.parse(xhr.responseText); } catch (e) { reject(e); } if (response) { resolve(response, xhr.status, xhr); } } else { reject(xhr); } } }; xhr.open('GET', 'https://api.douban.com/v2/user/aisk', true); xhr.setRequestHeader("Content-Type", "text/plain"); xhr.send(data); }); }; (async function() { try { let result = await fetchDoubanApi(); console.log(result); } catch (e) { console.log(e); } })();
ES7 還在草案階段,那現在想用這個特性怎麼辦?可以嘗試 google 的一個 JavaScript 預編譯器 traceur,可以將高版本的 JavaScript 編譯為 ES5 程式碼,已經實驗性的支援了 async/await (需要使用 --experimental 來指定開啟)。traceur 可以直接在後端使用,也可以在瀏覽器中使用。另外如果只在 node.js 環境中使用的話,還有一些 polyfill 模組,比如這個。
更多文章參見: http://aisk.me/using-async-await-to-avoid-callback-hell/