Nodejs異步回調的處理方法總結

前些天弄了篇JavaScript異步問題解決方案,在nodejs中異步回調的處理方法也大同小異,我們來看看吧。

1. 前言

Nodejs最大的亮點就在于事件驅動, 非阻塞I/O 模型,這使得Nodejs具有很強的并發處理能力,非常適合編寫網絡應用。在Nodejs中大部分的I/O操作幾乎都是異步的,也就是我們處理I/O的操作結果基本上都需要在回調函數中處理,比如下面的這個讀取文件內容的函數:

fs.readFile('/etc/geekjc', function (err, data) {
  if (err) throw err;
  console.log(data);
});

那,我們讀取兩個文件,將這兩個文件的內容合并到一起處理怎么辦呢?大多數接觸js不久的人可能會這么干:(我就是這么做的...)

fs.readFile('/etc/geekjc', function (err, data) {
  if (err) throw err;
  fs.readFile('/etc/geekjc-user', function (err, data2) {
    if (err) throw err;
    // 在這里處理data和data2的數據
  });
});

那要是處理多個類似的場景,豈不是回調函數一層層的嵌套啊,這就是大家常說的回調金字塔或回調地獄(http://callbackhell.com/)的問題,也是讓js小白最為頭疼的問題。

這種層層嵌套的代碼給開發帶來了很多問題,主要體現在:

  • 代碼可能性變差
  • 調試困難
  • 出現異常后難以排查

我們來看看如何優雅的處理以上異步回調問題。

2. 通過遞歸處理異步回調

我們可以使用遞歸作為代碼的執行控制工具。把需要執行的操作封裝到一個函數中,在回調函數中通過遞歸調用控制代碼的執行流程,廢話不多說,上個代碼吧:

var fs = require('fs');
// 要處理的文件列表
var files = ['file1', 'file2', 'file3'];
function parseFile () {
  if (files.length == 0) {
    return;
  }
  var file = files.shift();
  fs.readFile(file, function (err, data) {
    // 這里處理文件數據
    parseFile();  // 處理完畢后,通過遞歸調用處理下一個文件
  });
}

// 開始處理
parseFile();

以上代碼已依次處理數組中的文件為例,介紹了通過遞歸的方式控制代碼的執行流程。

應用到一些簡單的場景中還是不錯的,比如:我們將一個數組中的數據,依次保存到數據庫中就可以采用這種方式。

通過遞歸的方式可以解決一些簡單的異步回調問題。不過對于處理復雜的異步回調還是顯得有些無能為力(如需要同步多個異步操作的結果)。

3. 采用Async、Q、Promise等第三方庫處理異步回調

為了更好的處理嵌套回調的問題,可以考慮采用一些第三方專門處理異步的庫,當然有能力的完全可以自己寫個異步處理的輔助工具。

比較常用的處理異步的庫有:async,q還有promise。從npmjs.org網站上來看,async的火熱程度最高。以前用過async,確實也挺方便的,各種異步處理的控制流實現的也挺好。

我們將最初的同時讀取兩個文件的代碼使用async處理下,示例如下:

var async = require('async')
  , fs = require('fs');
async.parallel([
  function(callback){
    fs.readFile('/etc/passwd', function (err, data) {
      if (err) callback(err);
      callback(null, data);
    });
  },
  function(callback){
    fs.readFile('/etc/passwd2', function (err, data2) {
      if (err) callback(err);
      callback(null, data2);
    });
  }
],
function(err, results){
  // 在這里處理data和data2的數據,每個文件的內容從results中獲取
});

通過async模塊,可以很好的控制異步的執行流程了,也算是解決了層層回調的問題,代碼比以前算是清晰了些,不過依舊還是離不開回調函數。

想想如果能夠在不使用回調函數的情況下,處理異步,豈不是很爽,接下來,我們談談使用ES6的新特性來實現這一目標。

4. 擁抱ES6,替代回調函數,解決回調地獄問題

話說EcmaScript Harmony (ES6)給js引入了不少新特性,對ES6不太了解的同學,可以自行百度一下。

在nodejs中使用ES6的新特性,需要用v0.11.x以上的版本才行。

本文介紹的是使用Generator特性替代回調函數,對Generator不了解?可以看看這里。

這里用到了co和thunkify兩個模塊,大家使用npm install命令安裝之。

還是以本文剛開始提到的問題為例,使用generator特性的實例代碼如下:

var fs = require('fs')
  , co = require('co')
  , thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

co(function *() {
  var test1 = yield readFile('test1.txt');
  var test2 = yield readFile('test2.txt');
  var test = test1.toString() + test2.toString();
  console.log(test);
})();

處理代碼中的異常也是很簡單的,只需要這樣就OK了:


try {
  var test1 = yield readFile('test1.txt');
} catch (e) {
  // 在這里處理異常
}

這種代碼是不是優雅很多了?像寫同步代碼一樣處理異步,是不是很爽!

nodejs領域中進行Web開發,最火的框架莫過于express了,值得一提的是express的核心成員TJ大神有領導了一個新的Web框架——koa,宣稱是下一代的Web開發框架,koa真是借助了ES6的generator這一特性,讓我們在開發Web系統的時候避免陷入層層的回調用。

5. async/await時代

先看看采用generator處理的代碼:

let co = require("co");

co(function *(){
    let db, collection, result; 
    let person = {name: "geekjc"};
    try{
        db = yield mongoDb.open();
        collection = yield db.collection("users");
        result = yield collection.insert(person);
    }catch(e){
        console.error(e.message);
    }
    console.log(result);
});

我們關注到ES7的async/await,才發現這才是我們想要的!我們將上面的代碼小小改寫一下。

async function insertData(person){
    let db, collection, result; 
    try{
        db = await mongoDb.open();
        collection = await db.collection("users");
        result = await collection.insert(person);
    }catch(e){
        console.error(e.message);
    }
    console.log(result);
} 

insertData({name: "geekjc"});

我們可以看到inserData是一個真正的函數,是我們可以直接去調用而無需啟動器驅動的。當然內部我們也可以感受到處理yield變成了await以外,并沒有很大區別。async/await,更符合我們異步編程的語義。

那么問題來了,how to use it?

5.1 使用

我們一開始就說過,babel已經支持async的transform了,所以我們使用的時候引入babel就行。當然server端和browser端,可以有不同的處理方法。在開始之前我們需要引入以下的package,preset-stage-3里就有我們需要的async/await的編譯文件。

$ npm install babel-core --save
$ npm install babel-preset-es2015 --save
$ npm install babel-preset-stage-3 --save

Browser端

Babel一開始的出現就是為了讓舊瀏覽器也能支持新的ES6特性,提升我們的開發體驗。所以在Babel一開始就是可以通過babel-cli終端進行編譯的。或者引入babel文件在瀏覽器端進行編譯。當然這些都不是我最推薦的,所以我就帶過不說啦。在前端靜態資源配置里,webpack是現在比較好的解決方案,它支持靜態資源的模塊依賴,打包合并,還有語言的預處理,當然在這里我們就是指babel的處理。

// webpack.config.js
// 省略上面的文件輸入輸出的配置,直接看模塊加載器的配置
module: {
    loaders: [
        {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            loader: "babel",
            query: {
              presets: ['es2015', 'stage-3']
            }
        },
    ]
}

這樣我們就可以愉快的使用了。

Server端

相對來說,后端比前端需要處理的異步IO地方多得多,也是更加需要這個。那我們在Server端又如何引入babel呢?

其實最簡單也是最麻煩的方法就是,直接把js文件通過babel編譯出新的文件再來使用。當然也就免不了冗余文件了,眼不見心不煩,還是換一個方法吧。

我們使用官方提供的require hook方法,顧名思義就是通過require進來后,接下來的文件進行require的時候都會經過Babel的處理。因為我們知道CommonJs是同步的模塊依賴,所以也是可行的方法。我們需要多一個用于啟動的js文件,一個真正執行程序的js文件。

// index.js 
// 用于引入babel,并且啟動app.js

require("babel-core/register");
require("./app.js");

配置完hook之后,我們就配置babel的.babelrc文件,它是一個json格式的文件。es2015看情況配置,如果是已經是Node5.0版本,就無需再進行編譯。

{
  "presets": ["stage-3", "es2015"]
}

最后我們的異步函數代碼,寫在app.js里即可。

6. 相關文章

JavaScript異步問題解決方案

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

推薦閱讀更多精彩內容