寫這個問題是由於最近看到一些初學者用回調用的不亦樂乎,最後代碼左調來又調去很不直觀。node
首先上結論:推薦使用async/await或者co/yield,其次是promise,再次是事件,回調不要使用。promise
接下來是解析,爲何我會有這樣的結論緩存
首先是回調,理解上最簡單,就是我把任務分配出去,當你執行完了我就能從你那裏拿到結果執行相應的回調,異步
這裏演示一個對setTimeout的封裝,規定時間後打印相應結果並執行回調函數async
而且這個函數傳給回調函數的參數符合node標準,第一個爲error信息,若是出錯error不爲null,正常執行則爲null函數
var i = 0; function sleep(ms, callback) { setTimeout(function () { console.log('我執行完啦!'); i++; if (i >= 2) callback(new Error('i大於2'), null); else callback(null, i); }, ms); } sleep(3000, function (err,val) { if(err) console.log('出錯啦:'+err.message); else console.log(val); }) //執行結果:3s後打印 "我執行完啦","1"
這樣的代碼看上去並不會很不舒服,並且也比較好理解,可是假如我要暫停屢次呢ui
調用的代碼就變成了以下:spa
sleep(1000, function (err, val) { if (err) return console.log(err.message);; console.log(val); sleep(1000, function (err, val) { if (err) return console.log(err.message); console.log(val); sleep(1000, function (err, val) { if (err) console.log(err.message); else console.log(val); }) }) })
能夠看得出來,嵌套得很深,你能夠把這三次操做當作三個異步任務,而且還有可能繼續嵌套下去,這樣的寫法顯然是反人類的。code
嵌套得深首先一個不美觀看的很不舒服,第二個若是回調函數出錯了也難以判斷在哪裏出錯的。對象
因而改進方法就是事件監聽,每次調用一個異步函數都返回一個EventEmitter對象,並在執行成功時調用done事件,
失敗時調用error事件
var i = 0; function sleep(ms) { var emitter = new require('events')(); setTimeout(function () { console.log('我執行完啦!'); i++; if (i >= 2) emitter.emit('error', new Error('i大於2')); else emitter.emit('done', i); }, ms); } var emit = sleep(3000); emit.on('done',function (val) { console.log('成功:' + val); }) emit.on('error',function(err){ console.log('出錯了:' + err.message); })
這樣寫比以前的好處在於能添加多個回調函數,每一個回調函數都能得到值並進行相應操做。但這並無解決回調嵌套的問題,
好比這個函數屢次調用仍是必須寫在ondone的回調函數裏,看起來仍是很不方便。
因此比較廣泛的解決方案是Promise。
promise和事件相似,你能夠把它當作只觸發兩個事件的event對象,可是事件具備即時性,觸發以後這個狀態就不存在了,這個
事件已經觸發過了,你就再也拿不到值了,而promise不一樣,promise只有兩個狀態resolve和reject,當它觸發任何一個狀態後
它會將當前的值緩存起來,並在有回調函數添加進來的時候嘗試調用回調函數,若是這個時候尚未觸發resolve或者reject,那麼
回調函數會被緩存,等待調用,若是已經有了狀態(resolve或者reject),則馬上調用回調函數。而且全部回調函數在執行後都當即
被銷燬。
代碼以下:
var i = 0; //函數返回promise function sleep(ms) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('我執行好了'); i++; if (i >= 2) reject(new Error('i>=2')); else resolve(i); }, ms); }) } sleep(1000).then(function (val) { console.log(val); return sleep(1000) }).then(function (val) { console.log(val); return sleep(1000) }).then(function (val) { console.log(val); return sleep(1000) }).catch(function (err) { console.log('出錯啦:' + err.message); })
這個例子中,首先它將本來嵌套的回調函數展開了,如今看的更舒服了,而且因爲promise的冒泡性質,當promise鏈中的任意一個
函數出錯都會直接拋出到鏈的最底部,因此咱們統一用了一個catch去捕獲,每次promise的回調返回一個promise,這個promise
把下一個then看成本身的回調函數,並在resolve以後執行,或在reject後被catch出來。這種鏈式的寫法讓函數的流程比較清楚了,
拋棄了嵌套,終於能平整的寫代碼了。
但promise只是解決了回調嵌套的問題,並無解決回調自己,咱們看到的代碼依然是用回調阻止的。因而這裏就引入了async/await
關鍵字。
async/await是es7的新標準,而且在node7.0中已經獲得支持,只是須要使用harmony模式去運行。
async函數定義以下
async function fn(){ return 0; }
即便用async關鍵字修飾function便可,async函數的特徵在於調用return返回的並非一個普通的值,而是一個Promise對象,若是
正常return了,則返回Promise.resolve(返回值),若是throw一個異常了,則返回Promise.reject(異常)。也就是說async函數的返回
值必定是一個promise,只是你寫出來是一個普通的值,這僅僅是一個語法糖。
await關鍵字只能在async函數中才能使用,也就是說你不能在任意地方使用await。await關鍵字後跟一個promise對象,函數執行到await後會退出該函數,直到事件輪詢檢查到Promise有了狀態resolve或reject 才從新執行這個函數後面的內容。
首先我用剛剛的例子展現async/await的神奇之處
var i = 0; //函數返回promise function sleep(ms) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('我執行好了'); i++; if (i >= 2) reject(new Error('i>=2')); else resolve(i); }, ms); }) } (async function () { try { var val; val = await sleep(1000); console.log(val); val = await sleep(1000); console.log(val); val = await sleep(1000); console.log(val); } catch (err) { console.log('出錯啦:'+err.message); } } ())
看上去代碼是徹底同步的,每等待1s後輸出一次,而且在sleep返回的promise中狀態爲reject的時候還能被try...catch出來。
那麼這究竟是怎麼回事呢 咱們來看一張圖
這段代碼和剛剛的代碼同樣,只是在async函數被調用後輸出了一次"主程序沒有被調用",結果以下
咱們發現後面輸出的話是先打印的,這好像和咱們的代碼順不同,這是怎麼回事呢。
總的來講async/await是promise的語法糖,但它能將本來異步的代碼寫成同步的形式,try...catch也是比較友好的捕獲異常的方式
因此在從此寫node的時候儘可能多用promise或者async/await,對於回調就不要使用了,大量嵌套真的很反人類。