最近讀了樸靈老師的《深刻淺出NodeJS》中《異步編程》一章,並參考了一些有趣的文章。
在此作個筆記,記錄並鞏固學到的知識。html
異步I/O、事件驅動使得單線程的JavaScript得以在不阻塞UI的狀況下執行網絡、文件訪問功能,
且使之在後端實現了較高的性能。然而異步風格也引來了一些麻煩,其中比較核心的問題是:前端
函數嵌套過深node
JavaScript的異步調用基於回調函數,當多個異步事務多級依賴時,回調函數會造成多級的嵌套,代碼變成
金字塔型結構。這不只使得代碼變難看難懂,更使得調試、重構的過程充滿風險。git
異常處理程序員
回調嵌套不只僅是使代碼變得雜亂,也使得錯誤處理更復雜。es6
異步編程中可能拋出錯誤的狀況有兩種:github
異步函數錯誤編程
因爲異步函數是馬上返回的,異步事務中發生的錯誤是沒法經過try-catch來捕捉的,只能採用由調用方提供錯誤處理回調的方案來解決。
例如Node中常見的function (err, ...) {...}回調函數,就是Node中處理錯誤的約定:
即將錯誤做爲回調函數的第一個實參返回。
再好比HTML5中FileReader對象的onerror函數,會被用於處理異步讀取文件過程當中的錯誤。後端
回調函數錯誤數組
因爲回調函數執行時,異步函數的上下文已經不存在了,經過try-catch沒法捕捉回調函數內的錯誤。
可見,異步回調編程風格基本上廢掉了try-catch和throw。另外回調函數中的return也失去了意義,這會使咱們的程序必須依賴於反作用。
這使得JavaScript的三個語義失效,同時又得引入新的錯誤處理方案,若是沒有像Node那樣統一的錯誤處理約定,問題會變得更加麻煩。
下面對幾種解決方案的討論主要集中於上面提到的兩個核心問題上,固然也會考慮其餘方面的因素來評判其優缺點。
首先是Node中很是著名的Async.js,這個庫可以在Node中展露頭角,恐怕也得歸功於Node統一的錯誤處理約定。
而在前端,一開始並無造成這麼統一的約定,所以使用Async.js的話可能須要對現有的庫進行封裝。
Async.js的其實就是給回調函數的幾種常見使用模式加了一層包裝。好比咱們須要三個先後依賴的異步操做,採用純回調函數寫法以下:
asyncOpA(a, b, (err, result) => { if (err) { handleErrorA(err); } asyncOpB(c, result, (err, result) => { if (err) { handleErrorB(err); } asyncOpB(d, result, (err, result) => { if (err) { handlerErrorC(err); } finalOp(result); }); }); });
若是咱們採用async庫來作:
async.waterfall([ (cb) => { asyncOpA(a, b, (err, result) => { cb(err, c, result); }); }, (c, lastResult, cb) => { asyncOpB(c, lastResult, (err, result) => { cb(err, d, result); }) }, (d, lastResult, cb) => { asyncOpC(d, lastResult, (err, result) => { cb(err, result); }); } ], (err, finalResult) => { if (err) { handlerError(err); } finalOp(finalResult); });
能夠看到,回調函數由原來的橫向發輾轉變爲縱向發展,同時錯誤被統一傳遞到最後的處理函數中。
其原理是,將函數數組中的後一個函數包裝後做爲前一個函數的末參數cb傳入,同時要求:
每個函數都應當執行其cb參數;
cb的第一個參數用來傳遞錯誤。
咱們能夠本身寫一個async.waterfall的實現:
let async = { waterfall: (methods, finalCb = _emptyFunction) => { if (!_isArray(methods)) { return finalCb(new Error('First argument to waterfall must be an array of functions')); } if (!methods.length) { return finalCb(); } function wrap(n) { if (n === methods.length) { return finalCb; } return function (err, ...args) { if (err) { return finalCb(err); } methods[n](...args, wrap(n + 1)); } } wrap(0)(false); } };
Async.js還有series/parallel/whilst等多種流程控制方法,來實現常見的異步協做。
Async.js的問題是:
在外在上依然沒有擺脫回調函數,只是將其從橫向發展變爲縱向,仍是須要程序員熟練異步回調風格。
錯誤處理上仍然沒有利用上try-catch和throw,依賴於「回調函數的第一個參數用來傳遞錯誤」這樣的一個約定。
ES6的Promise來源於Promise/A+。使用Promise來進行異步流程控制,有幾個須要注意的問題,
在We have a problem with promises一文中有很好的總結。
把前面提到的功能用Promise來實現,須要先包裝異步函數,使之能返回一個Promise:
function toPromiseStyle(fn) { return (...args) => { return new Promise((resolve, reject) => { fn(...args, (err, result) => { if (err) reject(err); resolve(result); }) }); }; }
這個函數能夠把符合下述規則的異步函數轉換爲返回Promise的函數:
回調函數的第一個參數用於傳遞錯誤,第二個參數用於傳遞正常的結果。
接着就能夠進行操做了:
let [opA, opB, opC] = [asyncOpA, asyncOpB, asyncOpC].map((fn) => toPromiseStyle(fn)); opA(a, b) .then((res) => { return opB(c, res); }) .then((res) => { return opC(d, res); }) .then((res) => { return finalOp(res); }) .catch((err) => { handleError(err); });
經過Promise,原來明顯的異步回調函數風格顯得更像同步編程風格,咱們只須要使用then方法將結果傳遞下去便可,同時return也有了相應的意義:
在每個then的onFullfilled函數(以及onRejected)裏的return,都會爲下一個then的onFullfilled函數(以及onRejected)的參數設定好值。
如此一來,return、try-catch/throw均可以使用了,但catch是以方法的形式出現,仍是不盡如人意。
ES6引入的Generator能夠理解爲可在運行中轉移控制權給其餘代碼,並在須要的時候返回繼續執行的函數。利用Generator能夠實現協程的功能。
將Generator與Promise結合,能夠進一步將異步代碼轉化爲同步風格:
function* getResult() { let res, a, b, c, d; try { res = yield opA(a, b); res = yield opB(c, res); res = yield opC(d); return res; } catch (err) { return handleError(err); } }
然而咱們還須要一個能夠自動運行Generator的函數:
function spawn(genF, ...args) { return new Promise((resolve, reject) => { let gen = genF(...args); function next(fn) { try { let r = fn(); if (r.done) { resolve(r.value); } Promise.resolve(r.value) .then((v) => { next(() => { return gen.next(v); }); }).catch((err) => { next(() => { return gen.throw(err); }) }); } catch (err) { reject(err); } } next(() => { return gen.next(undefined); }); }); }
用這個函數來調用Generator便可:
spawn(getResult) .then((res) => { finalOp(res); }) .catch((err) => { handleFinalOpError(err); });
可見try-catch和return實際上已經以其本來面貌回到了代碼中,在代碼形式上也已經看不到異步風格的痕跡。
ES7中將會引入async function和await關鍵字,利用這個功能,咱們能夠輕鬆寫出同步風格的代碼,
同時依然能夠利用原有的異步I/O機制。
採用async function,咱們能夠將以前的代碼寫成這樣:
async function getResult() { let res, a, b, c, d; try { res = await opA(a, b); res = await opB(c, res); res = await opC(d); return res; } catch (err) { return handleError(err); } } getResult();
和Generator & Promise方案看起來沒有太大區別,只是關鍵字換了換。
實際上async function就是對Generator方案的一個官方承認,將之做爲語言內置功能。
async function的缺點是:
await只能在async function內部使用,所以一旦你寫了幾個async function,
或者使用了依賴於async function的庫,那你極可能會須要更多的async function。
目前處於提案階段的async function尚未獲得任何瀏覽器或Node.JS/io.js的支持。
Babel轉碼器也須要打開實驗選項,而且對於不支持Generator的瀏覽器來講,
還須要引進一層厚厚的regenerator runtime,想在前端生產環境獲得應用還須要時間。
1. A Study on Solving Callbacks with JavaScript Generators
5. We have a problem with promises