標籤:自己 並且 推薦 ror blog 解決方案 方案 失敗 完全
寫這個問題是因為最近看到一些初學者用回調用的不亦樂乎,最後代碼左調來又調去很不直觀。
首先上結論:推薦使用async/await或者co/yield,其次是promise,再次是事件,回調不要使用。
接下來是解析,為什麼我會有這樣的結論
首先是回調,理解上最簡單,就是我把任務分配出去,當你執行完了我就能從你那裡拿到結果執行相應的回調,
這裡示範一個對setTimeout的封裝,規定時間後列印相應結果並執行回呼函數
並且這個函數傳給回呼函數的參數符合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"
這樣的代碼看上去並不會很不舒服,而且也比較好理解,但是假如我要暫停多次呢
調用的代碼就變成了如下:
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); }) })})
可以看得出來,嵌套得很深,你可以把這三次操作看成三個非同步任務,並且還有可能繼續嵌套下去,這樣的寫法顯然是反人類的。
嵌套得深首先一個不美觀看的很不舒服,第二個如果回呼函數出錯了也難以判斷在哪裡出錯的。
於是改進方法就是事件監聽,每次調用一個非同步函數都返回一個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;//函數返回promisefunction 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;//函數返回promisefunction 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,對於回調就不要使用了,大量嵌套真的很反人類。
node.js非同步控制流程程 回調,事件,promise和async/await