本文转载自:众成翻译
译者:边城
链接:http://www.zcfy.cc/article/2442
原文:https://blog.risingstack.com/node-js-async-best-practices-avoiding-callback-hell-node-js-at-scale/
本文涵盖了处理 Node.js 异步操作的一些工具和技术:async.js、Promise、generator 和 异步函数。
阅读这篇文章之后你会知道如何避免臭名昭著的回调地狱!
Node.js at Scale 是系列文章,专注于使用大型 Node.js 设施的公司和高级 Node 开发者。章节包括:
使用 npm
Node.js 深度探索
构建
Node.js 异步最佳实践 (本文)
命令查询责任隔离
测试
单元测试
端到端测试
生产环境的 Node.js
监控 Node.js 应用
调试 Node.js 应用
分析 Node.js 应用
微服务
请求签名
分页式跟踪
API 网关
Node.js 中的异步编程
之前我们学习了关于JavaScript 中的异步编程的知识,也了解了Node.js 事件循环如何运作。
如果你还没有读这些文章,我强烈建议你去读一读!
Node.js 异步编程的问题
Node.js 本身是单线程的,但有些任务以并行的方式进行 —— 这受益于它的异步特性。
但并行的实际意义是什么?
我们在单线程VM中进行开发,但本质上来说我们不会因为I/O而阻塞,它会并行处理,这受益于 Node.js 的事件驱动 API。
来看一些基本的模块,学习如何通过 Node.js 内建的解决方案和一些第三方库来写一些高效利用资源、非阻塞的代码。
经典方法 - 回调
看看这些简单的异步操作。它们没做什么特别的事情,只是设置了一个定时器并在定时结束的时候调用一个函数。
function fastFunction (done) {
setTimeout(function () {
done()
}, 100)
}
function slowFunction (done) {
setTimeout(function () {
done()
}, 300)
}
看起来容易,是吗?
高阶函数可以通过一个基本的模式,嵌入回调,顺序或并行地执行。——但使用这个方法会导致难以控制的回调地狱。
function runSequentially (callback) {
fastFunction((err, data) => {
if (err) return callback(err)
console.log(data) // 结果 a
slowFunction((err, data) => {
if (err) return callback(err)
console.log(data) // 结果 b
// 这里继续执行其它任务
})
})
}
"永远不要使用嵌入回调的方式来处理异步 #nodejs 操作!" —— @RisingStack
通过控制流管理避免回调地狱
要成为一个高效的 Node.js 开发者,你必须避免不断增加的缩进级别,书写简洁且易读的代码,以及处理复杂的流程。
让我向你介绍一些库,它们能组织代码,使之简洁,而且易于维护!
#1: Async 模块
Async 是一个工具,它向JavaScript异步编程提供了直接而强大的功能。
Async 包含了错误作为回调第一个参数的异步流程控制的常见模式。
来看看前面的例子用 Async 来实现是什么样!
async.waterfall([fastFunction, slowFunction], () => {
console.log('done')
})
这是什么魔法?
实事上,并没有魔法。 你自己也可以很容易用 Async 实现并行任务并等待它们完成。
来看看 Async 在背后做了什么!
// 来自 https://github.com/caolan/async/blob/master/lib/waterfall.js
function(tasks, callback) {
callback = once(callback || noop);
if (!isArray(tasks)) return callback(new Error('First argument to waterfall must be an array of functions'));
if (!tasks.length) return callback();
var taskIndex = 0;
function nextTask(args) {
if (taskIndex === tasks.length) {
return callback.apply(null, [null].concat(args));
}
var taskCallback = onlyOnce(rest(function(err, args) {
if (err) {
return callback.apply(null, [err].concat(args));
}
nextTask(args);
}));
args.push(taskCallback);
var task = tasks[taskIndex++];
task.apply(null, args);
}
nextTask([]);
}
本质上来说,函数中注入了新的回调,所以 Async 知道函数什么时候完成。
#2: 使用 co - 基于 generator 的 Node.js 流程控制
如果你不喜欢总是回调,co 会是个不错的选择。
co 是一个基于 generator 的流程控制工作,可用于 Node.js 和浏览器。它可以通过 Promise 让你的非阻塞代码以一种漂亮的方式呈现。
co 是个非常不错的选择,它利用 generator 函数 来使用 Promise,不再需要自己实现迭代器。
const fastPromise = new Promise((resolve, reject) => {
fastFunction(resolve)
})
const slowPromise = new Promise((resolve, reject) => {
slowFunction(resolve)
})
co(function * () {
yield fastPromise
yield slowPromise
}).then(() => {
console.log('done')
})
现在这个阶段,我建议使用 co。目前大家都在期待的 Node.js async/await 功能只能在非稳定版本的 Node.js v7.x 中使用。不过如果你已经在使用 Promise,要从 co 切换到 async 函数是件非常容易的事情。
这是 Promise 上的语法糖,和 generator 一起使用可以消除回调问题,甚至还能帮助建立漂亮的流程控制结构。它使得写异步代码就跟写同步代码一样,不是吗?
稳定的 Node.js 分支会在不久以后加入 async/await 功能。那时候 co 就可以退出历史舞台了。
流程控制实践
我们已经学习了一些工具和技巧来处理异步。是时候进行一些基本的控制流程来改善代码了,它会让代码简洁而且高效。
现在跟随示例,写一个 Web 应用的路由处理器 handler
。这个处理器中需要进行三个步骤:validateParams
、dbQuery
和 serviceCall
。
如果你在不使用任何工具的情况下写这段代码,最终写出来的东西肯定不怎么好看:
// validateParams、dbQuery、serviceCall 都是高阶函数
// 不要
function handler (done) {
validateParams((err) => {
if (err) return done(err)
dbQuery((err, dbResults) => {
if (err) return done(err)
serviceCall((err, serviceResults) => {
done(err, { dbResults, serviceResults })
})
})
})
}
我们之前了解过,这种情况可以使用 Async 库来重构代码:
// validateParams、dbQuery、serviceCall都是高阶函数
function handler (done) {
async.waterfall([validateParams, dbQuery, serviceCall], done)
}
进一步用 Promise 重构:
// validateParams、dbQuery、serviceCall 都是 Thunk 函数
function handler () {
return validateParams()
.then(dbQuery)
.then(serviceCall)
.then((result) => {
console.log(result)
return result
})
}
[译者注] 关于 thunk 的含义,请参考阮一峰老师的 Thunk函数的含义和用法
也可以用 co 加强的 generator 和 Promise 来实现:
// validateParams、dbQuery、serviceCall 都是 Thunk 函数
const handler = co.wrap(function * () {
yield validateParams()
const dbResults = yield dbQuery()
const serviceResults = yield serviceCall()
return { dbResults, serviceResults }
})
这段代码感觉很像是“同步”代码,但它不是同步的,它会在一个异步工作完成之后再继续进行。
现在来看看如何用 async/await
来改写这段代码。
// validateParams、dbQuery、serviceCall 都是 Thunk 函数
async function handler () {
await validateParams()
const dbResults = await dbQuery()
const serviceResults = await serviceCall()
return { dbResults, serviceResults }
})
Node.js 中异步的规则
非常幸运,Node.js 中写线程案例的代码并不麻烦,你只需要遵循一些规则就可以了:
作为一个经验法则,尽可能使用异步 API 而不是同步 API。因为非阻塞的方式比同步的方式性能更好。
总是选用最合适的流程控制,或者组合的流程控制,以减少等待 I/O 完成的时间。
你可以在这个 代码库 中找到这篇文章的所有代码。
如果你有针对这篇文章的问题或建议,请通过评论告诉我们。
我们会在 Node.js at Scale 系列的一个部分继续了解 示例讲解事件来源