Node.js異步編程進化論

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或從新修改使用,但需註明來源。署名 4.0 國際 (CC BY 4.0)javascript

Node.js異步編程callback

咱們知道,Node.js中有兩種事件處理方式,分別是callback(回調)和EventEmitter(事件發射器)。本文首先介紹的是callback前端

error-first callback 錯誤優先是Node.js回調方式的標準。java

第一個參數是error,後面的參數纔是結果。node

咱們以現實生活中去面試來舉個🌰,面試成功咱們漏出洋溢的笑容,面試失敗咱們就哭並嘗試找到失敗的緣由。es6

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官方造成了以下規範:npm

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的表現。redux

同時去兩家公司面試,當兩家面試都成功時咱們纔會開心,看下面這個🌰promise

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最終狀態根據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,仍是Promiseasync/await只用短短几行代碼便實現了異步流程控制。

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

實踐經驗總結

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

參考:

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

❤️看完三件事

1.看到這裏了就點個贊支持下吧,你的點贊是我創做的動力。

2.關注公衆號前端食堂,你的前端食堂,記得按時吃飯!

3.入冬了,多穿衣服不要着涼~!

tongobama.png

相關文章
相關標籤/搜索