Node.js非同步編程進化論
- 2020 年 3 月 28 日
- 筆記
Node.js非同步編程callback
我們知道,Node.js中有兩種事件處理方式,分別是callback
(回調)和EventEmitter
(事件發射器)。本文首先介紹的是callback
。
error-first callback
錯誤優先是Node.js回調方式的標準。
第一個參數是error
,後面的參數才是結果。
我們以現實生活中去面試來舉個?,面試成功我們漏出洋溢的笑容,面試失敗我們就哭並嘗試找到失敗的原因。
try { interview(function() { console.log('smile'); }); } catch(e) { console.log('cry', e); } function interview(callback) { setTimeout(() => { if (Math.random() < 0.1) { callback('success'); } else { throw new Error('fail'); } }, 500); }
如上程式碼運行後,try/catch
並不像我們所想,它並沒有抓取到錯誤,錯誤反而被拋到了Node.js
全局,導致程式崩潰。(是由於Node.js的每一個事件循環都是一個全新的調用棧Call Stack)
為了解決上面的問題,Node.js官方形成了如下規範:
interview(function (res) { if (res) { return console.log('cry'); } console.log('smile'); }) function interview (callback) { setTimeout(() => { if (Math.random() < 0.8) { callback(null, 'success'); } else { callback(new Error('fail')); } }, 500); }
回調地獄Callback hell
XX大廠有三輪面試,看下面的?
interview(function (err) { if (err) { return console.log('cry at 1st round'); } interview(function (err) { if (err) { return console.log('cry at 2nd round'); } interview(function (err) { return console.log('cry at 3rd round'); }) console.log('smile'); }) }) function interview (callback) { setTimeout(() => { if (Math.random() < 0.1) { callback(null, 'success'); } else { callback(new Error('fail')); } }, 500); }
我們再來看並發情況下callback的表現。
同時去兩家公司面試,當兩家面試都成功時我們才會開心,看下面這個?
var count = 0; interview(function (err) { if (err) { return console.log('cry'); } count++; }) interview(function (err) { if (err) { return console.log('cry'); } count++; if (count) { //當count滿足一定條件時,面試都通過 //... return console.log('smile'); } }) function interview (callback) { setTimeout(() => { if (Math.random() < 0.1) { callback(null, 'success'); } else { callback(new Error('fail')); } }, 500); }
非同步邏輯的增多隨之而來的是嵌套深度的增加。如上的程式碼是有很多缺點的:
- 程式碼臃腫,不利於閱讀與維護
- 耦合度高,當需求變更時,重構成本大
- 因為回調函數都是匿名函數導致難以定位bug
為了解決回調地獄,社區曾提出了一些解決方案。
1.async.js npm包,是社區早期提出的解決回調地獄的一種非同步流程式控制制庫。 2.thunk 編程範式,著名的
co
模組在v4以前的版本中曾大量使用Thunk函數。Redux中也有中間件redux-thunk
。
不過它們都退出了歷史舞台。
畢竟軟體工程沒有銀彈,取代他們的方案是Promise
Promise
Promise/A+規範鎮樓,ES6採用的這個規範實現的Promise。
Promise 是非同步編程的一種解決方案,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
簡單說,Promise就是當前事件循環不會得到結果,但未來的事件循環會給到你結果。
毫無疑問,Promise是一個渣男。
Promise也是一個狀態機,只能從pending
變為以下狀態(一旦改變就不能再變更)
- fulfilled(本文稱為resolved)
- rejected
// nodejs 不會列印狀態 // Chrome控制台中可以 var promise = new Promise(function(resolve, reject){ setTimeout(() => { resolve(); }, 500) }) console.log(promise); setTimeout(() => { console.log(promise); }, 800); // node.js中 // promise { <pending> } // promise { <undefined> } // 將上面程式碼放入閉包中扔到google控制台里 // google中 // Promise { <pending> } // Promise { <resolved>: undefined }
Promise
- then
- catch
resolved
狀態的Promise會回調後面的第一個.then
rejected
狀態的Promise會回調後面的第一個.catch
任何一個rejected
狀態且後面沒有.catch
的Promise,都會造成瀏覽器/node環境
的全局錯誤。
Promise比callback優秀的地方,是可以解決非同步流程式控制制問題。
(function(){ var promise = interview(); promise .then((res) => { console.log('smile'); }) .catch((err) => { console.log('cry'); }); function interview() { return new Promise((resoleve ,reject) => { setTimeout(() => { if (Math.random() > 0.2) { resolve('success'); } else { reject(new Error('fail')); } }, 500); }); } })();
執行then
和catch
會返回一個新的Promise,該Promise最終狀態根據then
和catch
的回調函數的執行結果決定。我們可以看下面的程式碼和列印出的結果:
(function(){ var promise = interview(); var promise2 = promise .then((res) => { throw new Error('refuse'); }); setTimeout(() => { console.log(promise); console.log(promise2); }, 800); function interview() { return new Promise((resoleve ,reject) => { setTimeout(() => { if (Math.random() > 0.2) { resolve('success'); } else { reject(new Error('fail')); } }, 500); }); } })(); // Promise { <resolved>: "success"} // Promise { <rejected>: Error:refuse }
如果回調函數最終是
throw
,該Promise是rejected
狀態。 如果回調函數最終是return
,該Promise是resolved
狀態。 但如果回調函數最終return
了一個Promise,該Promise會和回調函數return Promise
狀態保持一致。
Promise解決回調地獄
我們來用Promise重新實現一下上面去大廠三輪面試程式碼。
(function() { var promise = interview(1) .then(() => { return interview(2); }) .then(() => { return interview(3); }) .then(() => { console.log('smile'); }) .catch((err) => { console.log('cry at' + err.round + 'round'); }); function interview (round) { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.2) { resolve('success'); } else { var Error = new Error('fail'); error.round = round; reject(error); } }, 500); }); } })();
與回調地獄相比,Promise實現的程式碼通透了許多。
Promise在一定程度上把回調地獄變成了比較線性的程式碼,去掉了橫向擴展,回調函數放到了then中,但其仍然存在於主流程上,與我們大腦順序線性的思維邏輯還是有出入的。
Promise處理並發非同步
(function() { Promise.all([ interview('Alibaba'), interview('Tencent') ]) .then(() => { console.log('smile'); }) .catch((err) => { console.log('cry for' + err.name); }); function interview (name) { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.2) { resolve('success'); } else { var Error = new Error('fail'); error.name = name; reject(error); } }, 500); }); } })();
上面程式碼中的catch
是存在問題的。注意,它只能獲取第一個錯誤。
Generator
Generator和Generator Function是ES6中引入的新特性,是在Python、C#等語言中借鑒過來。
生成器的本質是一種特殊的迭代器。
function * doSomething() {}
如上所示,函數後面帶「*」的就是Generator。
function * doSomething() { interview(1); yield; // Line (A) interview(2); } var person = doSomething(); person.next(); // 執行interview1,第一次面試,然後懸停在Line(A)處 person.next(); // 恢復Line(A)點的執行,執行interview2,進行第二次次面試
next的返回結果
第一個person.next()返回結果是{value:'', done:false} 第二個person.next()返回結果是{value:'', done:true}
關於next的返回結果,我們要知道,如果done
的值為true
,即代表Generator里的非同步操作全部執行完畢。
為了可以在Generator中使用多個yield,TJ Holowaychuk
編寫了co
這個著名的ES6模組。co的源碼有很多巧妙的實現,大家可以自行閱讀。
async/await
Generator的弊端是沒有執行器,它本身是為了計算而設計的迭代器,並不是為了流程式控制制而生。co的出現較好的解決了這個問題,但是為什麼我們非要藉助於co而不直接實現呢?
async/await
被選為天之驕子應運而生。
async function
是一個穿越事件循環存在的function。
async function
實際上是Promise的語法糖封裝。它也被稱為非同步編程的終極方案-以同步的方式寫非同步。
await
關鍵字可以"暫停"async function
的執行。
await
關鍵字可以以同步的寫法獲取Promise的執行結果。
try/catch
可以獲取await
所得到的任意錯誤,解決了上面Promise中catch只能獲取第一個錯誤的問題。
async/await解決回調地獄
(async function () { try { await interview(1); await interview(2); await interview(3); } catch (e) { return console.log('cry at' + e.round); } console.log('smile'); })();
async/await處理並發非同步
(async function () { try { await Promise.all([interview(1), interview(2)]); } catch (e) { return console.log('cry at' + e.round); } console.log('smile'); })();
無論是相比callback
,還是Promise
,async/await
只用短短几行程式碼便實現了非同步流程式控制制。
遺憾的是,async/await
最終沒能進入ES7
規範(只能等到ES8
),但在Chrome V8
引擎里得以實現,Node.js v7.6
也集成了async
函數。
實踐經驗總結
在常見的Web應用中,在DAO層使用Promise較好,在Service層使用async函數較好。
參考:
- 狼書-更了不起的Node.js
- Node.js開發實戰