­

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);          });      }  })();

執行thencatch會返回一個新的Promise,該Promise最終狀態根據thencatch的回調函數的執行結果決定。我們可以看下面的程式碼和列印出的結果:

(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,還是Promiseasync/await只用短短几行程式碼便實現了非同步流程式控制制。

遺憾的是,async/await最終沒能進入ES7規範(只能等到ES8),但在Chrome V8引擎里得以實現,Node.js v7.6也集成了async函數。

實踐經驗總結

在常見的Web應用中,在DAO層使用Promise較好,在Service層使用async函數較好。

參考:

  • 狼書-更了不起的Node.js
  • Node.js開發實戰