doAsync1(function () {
doAsync2(function () {
doAsync3(function () {
doAsync4(function () {
})
})
})
在本文中,我們要編寫一個模塊,使用一一系列的工具和類庫來展示流控制是如何工作的。而且新版的Node帶來了新的有潛力的解決方案,我們也會一探究竟。
問題
假設,我們需要找出某個目錄中最大的文件。var findLargest = require('./findLargest')
findLargest('./path/to/dir', function (er, filename) {
if (er) return console.error(er)
console.log('largest file was:', filename)
})
- 讀取給定文件夾中的全部文件
- 獲取每個文件的stats(狀態)
- 確定那個文件是最大的(如果多個文件都是最大的,選其中一個)
- 將最大文件的文件名傳給回調函數
嵌套代碼
第一種解決方案是嵌套的,看起來沒什麼恐怖的,並且邏輯還是看得懂的。var fs = require('fs')
var path = require('path')
module.exports = function (dir, cb) {
fs.readdir(dir, function (er, files) { [1]
if (er) return cb(er)
var counter = files.length
var errored = false
var stats = []
files.forEach(function (file, index) {
fs.stat(path.join(dir,file), function (er, stat) { [2]
if (errored) return
if (er) {
errored = true
return cb(er)
}
stats[index] = stat [3]
if (--counter == 0) { [4]
var largest = stats
.filter(function (stat) { return stat.isFile() }) [5]
.reduce(function (prev, next) { [6]
if (prev.size > next.size) return prev
return next
})
cb(null, files[stats.indexOf(largest)]) [7]
}
})
})
})
}
- 讀取目錄中的所有文件
- 讀取每個文件的stats。讀取的過程是並行的,因此我們使用一個counter,來追蹤I/O結束。同時我們還是了一個布爾變量errored,來防止多次出錯導致多次調用回調函數(cb)
- 收集每個文件的stats。注意我們在這裏設置了一個並行的數組(從files到stats)
- 檢查並行調用都完成了
- 過濾出普通文件(不包含鏈接、目錄等)
- Reduce整個列表,獲取最大的文件
- 根據stat獲取文件名,調用回調函數
模塊化
嵌套代碼的方案可以拆分成三個模塊單元:- 從目錄中讀取所有文件
- 從這些文件中獲取stats
- 處理stats和files,獲取最大的文件
function getStats (paths, cb) {
var counter = paths.length
var errored = false
var stats = []
paths.forEach(function (path, index) {
fs.stat(path, function (er, stat) {
if (errored) return
if (er) {
errored = true
return cb(er)
}
stats[index] = stat
if (--counter == 0) cb(null, stats)
})
})
}
function getLargestFile (files, stats) {
var largest = stats
.filter(function (stat) { return stat.isFile() })
.reduce(function (prev, next) {
if (prev.size > next.size) return prev
return next
})
return files[stats.indexOf(largest)]
}
var fs = require('fs')
var path = require('path')
module.exports = function (dir, cb) {
fs.readdir(dir, function (er, files) {
if (er) return cb(er)
var paths = files.map(function (file) { [1]
return path.join(dir,file)
})
getStats(paths, function (er, stats) {
if (er) return cb(er)
var largestFile = getLargestFile(files, stats)
cb(null, largestFile)
})
})
}
模塊化的解決方案使得代碼重用和測試更容易。核心的export方法也更容易理解。然而,我們還是手動管理並行的stat任務。讓我試試使用一些流控制的類庫,看看我們可以做些什麼?
Async
async模塊很流行,與Node的核心精神類似。我們來看看如何使用async來重構代碼:var fs = require('fs')
var async = require('async')
var path = require('path')
module.exports = function (dir, cb) {
async.waterfall([ [1]
function (next) {
fs.readdir(dir, next)
},
function (files, next) {
var paths =
files.map(function (file) { return path.join(dir,file) })
async.map(paths, fs.stat, function (er, stats) { [2]
next(er, files, stats)
})
},
function (files, stats, next) {
var largest = stats
.filter(function (stat) { return stat.isFile() })
.reduce(function (prev, next) {
if (prev.size > next.size) return prev
return next
})
next(null, files[stats.indexOf(largest)])
}
], cb) [3]
}
- async.waterfall提供瀑布式的流控制。每個操作產生的數據可以傳遞給下一個函數,通過next這個回調函數
- async.map允許我們並行對一系列的path調用fs.stat,並把一個結果數組傳遞給回調函數
- 最後一步後會調用回調函數cb;如果在整個運行過程中出錯了也會調用cb,不會它只會被調用一次
Promise
Promise提供了錯誤處理和函數式編程。我們如何使用Promise來解決這個問題呢?讓我們使用Q模塊試試(當然也可以使用其他Promise類庫):var fs = require('fs')
var path = require('path')
var Q = require('q')
var fs_readdir = Q.denodeify(fs.readdir) [1]
var fs_stat = Q.denodeify(fs.stat)
module.exports = function (dir) {
return fs_readdir(dir)
.then(function (files) {
var promises = files.map(function (file) {
return fs_stat(path.join(dir,file))
})
return Q.all(promises).then(function (stats) { [2]
return [files, stats] [3]
})
})
.then(function (data) { [4] var files = data[0]
var stats = data[1] var largest = stats
.filter(function (stat) { return stat.isFile() })
.reduce(function (prev, next) {
if (prev.size > next.size) return prev
return next
})
return files[stats.indexOf(largest)]
})
}
- Node內核並不是promise化的,轉化之
- Q.all同步執行所有stat,結果數組保持了原來的順序
- 最後把files和stat傳遞給next函數
var findLargest = require('./findLargest')
findLargest('./path/to/dir')
.then(function (er, filename) {
console.log('largest file was:', filename)
})
.catch(console.error)
儘管上面是這樣設計的,但是你也沒必要暴露promise化的接口。很多promise類庫也提供了暴露node風格接口的方法。在Q中,我們可以使用nodeify函數來實現。
我們不會深入介紹Promise,如果大家想了解更多的話,閱讀這篇文章。
Generator
本文一開始就說的,在這個領域來了一個新技術,在Node 0.11.2及以上版本中已經可以使用了——Generator!Generator是爲JavaScript設計的一種輕量級的協程。它通過yield關鍵字,可以控制一個函數暫停或者繼續執行Generator函數有一個特別的語法function* (),藉助這種超能力,我們還可以暫停或者繼續執行異步操作,使用像promise或者”thunks”這樣的結構來寫出看起來像同步的代碼。
Thunk函數是一種這樣的函數,返回一個回調來調用它自己。回調函數與典型的node回調函數有着一致的參數(例如error是第一個參數)。閱讀這裏瞭解更多。
讓我看一個例子,如何使用Generator來做異步的控制: TJ Holowaychuk的類庫co。下面是我們尋找最大文件的程序:
var co = require('co')
var thunkify = require('thunkify')
var fs = require('fs')
var path = require('path')
var readdir = thunkify(fs.readdir) [1]
var stat = thunkify(fs.stat)
module.exports = co(function* (dir) { [2] var files = yield readdir(dir) [3]
var stats = yield files.map(function (file) { [4]
return stat(path.join(dir,file))
})
var largest = stats
.filter(function (stat) { return stat.isFile() })
.reduce(function (prev, next) {
if (prev.size > next.size) return prev
return next
})
return files[stats.indexOf(largest)] [5]
})
- 既然Node核心函數並不是thunk化的,我們thunk之
- co接受一個Generator函數,在這個函數內部,可以使用yield關鍵字在任何地方暫停
- Generator函數直到readdir返回了才繼續執行。結果賦值給files變量
- co還能夠處理一系列並行的操作數組,結果按順序保存到stats這個數組中
- 返回最終結果
try {
var files = yield readdir(dir)
} catch (er) {
console.error('something happened whilst reading the directory')
}
Co還能非常優雅地支持數組、對象、嵌套Generator和Promise等等。
也涌現出了一些其他Generator模塊。Q模塊報了一個優雅的Q.async方法,行爲與co使用Generator一致。
總結
在本文中,我們研究了很多種不同的方案,來處理回調黑洞。其實也就是程序的流程控制。我個人對Generator的方案非常感興趣。我很好奇像koa這樣的新框架可以帶來什麼。想要本文使用到的代碼示例以及其他一些Generator例子?這裏有一個Github repo可以滿足你!本文首發於StrongLoop | Managing Node.js Callback Hell with Promises, Generators and Other Approaches。
原文:Managing Node.js Callback Hell