转自:http://zhuanlan.zhihu.com/FrontendMagazine/19750470
我们常常把这个问题叫做”回调黑洞”或”回调金字塔”:
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)
})
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]
}
})
})
})
}
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任务。让我试试使用一些流控制的类库,看看我们可以做些什么?
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]
}
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)]
})
}
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是为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]
})
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一致。