Node.js異步控制流:回調、事件、Promise和async/await

前言

對于Node.js的異步控制流,目前共計有四種常用的方式。較為經典的為callbackEventEmitter;在ES6中,加入了Promise;在ES7中加入了async/await。下面就逐個分析一下這四種常用的異步控制。

callback形式的異步控制

對于callback形式,即采用回調函數。在理解上,就是函數將任務分配出去,當任務完成之后,然后根據執行結果來進行相應的回調。可以看下面一個例子:

function sleep(ms, callback){
    setTimeout(function(){
        callback("finish")  //執行完之后,返回‘finish’。
    }, ms);
}

sleep(1000, function(val){
    console.log(val);
});

//輸出結果:finish。

這種形式十分容易理解,但是能夠解決大部分的問題。但是卻存在著致命的缺點。可以想象,如果需要在一段代碼中調用多次sleep()函數,將會出現下面的情況:

var i = 0;//記錄sleep()函數調用的次數
function sleep(ms, callback){
    setTimeout(function(){
        if(i < 2){
            i++;
            callback("finish", null);  
        }else{
            callback(null, new Error('i大于2'));
        }
    }, ms);
}
//第一次調用
sleep(1000, function (val, err) {
    if (err) console.log(err.message);
    else {
        console.log(val);
        //第二次調用
        sleep(1000, function (val, err) {
            if (err) console.log(err.message);
            else {
                console.log(val);
                //第三次調用
                sleep(1000, function (val, err) {
                    if (err) console.log(err.message);
                    else {
                        console.log(val);
                    }
                });
            }
        });
    }
});

//輸出結果分別為:finish,finish,i大于2。

由上面的例子可以看出,假如需要多次調用sleep()函數時,就會進行多次的嵌套。這種嵌套雖然可以使用,但是在代碼的閱讀、維護和美觀上面,都是反人類出現的。所以,在新標準ES6和ES7發布之后,將會逐漸摒棄這種寫法。

事件監聽形式的異步控制

對于callback的改進就是使用事件監聽的形式進行操作:每次調用異步函數都會返回一個EvetEmitter對象。在函數內部,可以根據需求來觸發不同的事件。對不同的事件進行監聽,然后做出相應的處理。具體代碼如下:

var i = 0;
var events = require('events');
var emitter = new events.EventEmitter();//創建事件監聽器的一個對象
function sleep(ms) {
    setTimeout(function () {
        i++;
        if (i < 2) {
            emitter.emit('done', 'finish');//觸發事件'done'
        } else {
            emitter.emit('error', new Error('i大于2'));//觸發事件'error'
        }
    }, ms);
}
var emit = sleep(1000);
//監聽事件'done'
emitter.on('done', function(val){
    console.log(val);
});
//監聽事件'error'
emitter.on('error', function(val){
    console.log(val);
});

通過事件監聽的形式進行異步處理,可以更加直觀和多樣性。但是和callback類似,并沒有解決嵌套的問題。例如需要調用多次sleep()函數,依然需要在函數emitter.on('done', function(val))中進行嵌套。

Promise對象進行異步控制處理

比較Promise和EventEmitter,可以發現兩個十分明顯的區別。對于EventEmitter,我們可以根據自己的需求來定義多種監聽事件,然后對不同的事件進行不同的處理;對于promise,則可以對將異步操作的結果進行返回,然后根據返回值來進行相應的操作。對于promise的詳細學習,可以通過在MDN查閱詳細且最新的資料。
代碼如下:

let num = 1
function sleep(ms) {
  //返回一個Promise對象
  return new Promise(function (resolev, reject) {
    setTimeout(function () {
      if (num < 1) {
        num++
        //如果成功,對resolve處理
        resolev(new Error('finish'))
      } else {
        //如果失敗,對reject操作
        reject('i大于2')
      }
    }, ms)
  })
}
sleep(300).then(value => {
  console.log(value)
  return sleep(300)
}).then(value => {
  console.log(value)
  return sleep(300)
}).then(value => {
  console.log(value)
  return sleep(300)
}).catch(function (error) {
  console.log(error)
})

和callback與EventEmmitter相比,Promise將異步處理的嵌套函數進行了展開,更利于閱讀和理解。當promise鏈中的任意一個函數出錯都會直接拋出到鏈的最底部,所以我們統一用了一個catch去捕獲,每次promise的回調返回一個promise,這個promise把下一個then當作自己的回調函數,并在resolve之后執行,或在reject后被catch出來。

async/await解決異步控制

雖然Promise將嵌套函數進行了展開,但是依然是鏈式函數,并沒有解決回調本身。因此在最新的ES7中,引入了async/await函數,來解決這個問題。async函數返回一個 Promise 對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操作完成,再接著執行函數體內后面的語句。
代碼如下:

let num = 1
function sleep(ms) {
  //返回一個Promise對象
  return new Promise(function (resolev, reject) {
    setTimeout(function () {
      if (num < 3) {
        num++
        //如果成功,對resolve處理
        resolev('finish')
      } else {
        //如果失敗,對reject操作
        reject(new Error('i大于2'))
      }
    }, ms)
  })
}
//用關鍵詞async修飾函數
async function asyncSleep() {
  try {
    let value
    //用await來修飾函數
    value = await sleep(300)
    console.log(value)
    value = await sleep(300)
    console.log(value)
    value = await sleep(300)
    console.log(value)
  } catch (error) {
    console.log(error)
  }
}
//調用函數
asyncSleep()

在上面的函數中,我們進行逐步分析

  • 我們首先將setTimeout函數進行封裝,讓其返回Promise對象。
  • 然后聲明一個用async修飾的函數,在里面調用sleep()函數。
  • 對于每次sleep()函數,我們用await關鍵詞進行修飾,表示下面的操作需要進行異步處理。
  • 聲明一個value變量來接受sleep()函數返回的Promise對象。
  • 最后用catch來對異常進行處理。

總的來說async/await是promise的語法糖,但它能將原本異步的代碼寫成同步的形式,try...catch也是比較友好的捕獲異常的方式所以在今后寫node的時候盡量多用promise或者async/await,對于回調就不要使用了,大量嵌套真的很反人類。

參考資料

node.js異步控制流程 回調,事件,promise和async/await
ECMAScript 6 入門, 阮一峰

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 異步編程對JavaScript語言太重要。Javascript語言的執行環境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,333評論 5 22
  • 本文首發在個人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云閱讀 1,701評論 0 3
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,747評論 0 5
  • javascript的運行機制是單線程處理,即只有上一個任務完成后,才會執行下一個任務,這種機制也被稱為“同步”。...
    我是xy閱讀 3,924評論 1 6
  • 異步編程模式在前端開發過程中,顯得越來越重要。從最開始的XHR到封裝后的Ajax都在試圖解決異步編程過程中的問題。...
    SCQ000閱讀 2,770評論 1 51