说起javascript编码的嵌套问题,大多数人会想到由于异步编程导致的回调函数嵌套:
//open db db.open((err, db) => { if(!err){ //create collection db.createCollection('myCollection', {safe:true}, (err, collection) => { if(err){ console.log(err); }else{ // query data... collection.find().toArray((err,docs) => { // do something... }); } }); }else{ console.log(err); } });
回调函数嵌套的代码不仅难以阅读维护,也难以解耦、扩展。
针对此情况,有多种解决办法,如:ES6的promise特性,eventproxy模块,async模块
现以async模块为例:
var async = require('async'); var openDB = (callback, results) => { //open db db.open((err, db) => { callback(err, db); } } var createCollection = (callback, results) => { //create collection results['openDB'].createCollection('myCollection', {safe:true}, (err, collection) => { callback(err, collection); }); } var queryData = (callback, results) => { //query data results['createCollection'].find().toArray((err,docs) => { callback(err, docs); }); } module.exports = (req, res) => { async.auto({ 'openDB': openDB, 'createCollection': ['openDB', createCollection], 'queryData': ['createCollection', queryData] }, (err, results) => { console.log(err); }); }
可以发现,使用async模块后,回调函数的嵌套问题得以解决,不同逻辑之间的依赖一目了然,逻辑显得十分清晰。
如果希望给以上逻辑中增加一个功能,将查询到的数据发送给客户端,也十分简单,只需要添加一个函数:
var sendData = (callback, results) => { res.send(results['queryData']); }
然后在async的入参中添加sendData:
module.exports = (req, res) => { async.auto({ 'openDB': openDB, 'createCollection': ['openDB', createCollection], 'queryData': ['createCollection', queryData], 'sendData': ['queryData', sendData] }, (err, results) => { console.log(err); }); }
这里出现一个问题,由于处于不同作用域,发送数据所需使用的res.send无法被sendData函数直接调用。
而sendData函数被async.auto调用的时候,会被强制传入callback, results两个参数。
因而无法使用bind传入res:
'sendData': ['queryData', sendData.bind(this, res)]
这种方案行不通。
最简单的解决方案是创建一个新的匿名函数,使用闭包扩大res的作用域:
module.exports = (req, res) => { async.auto({ 'openDB': openDB, 'createCollection': ['openDB', createCollection], 'queryData': ['createCollection', queryData], 'sendData': ['queryData', (callback, results) => { sendData(callback, results, res); }] }, (err, results) => { console.log(err); }); }
再改写sendData的入参:
var sendData = (callback, results, res) => { res.send(results['queryData']); }
问题看似已经解决,然而付出的代价是创建了一个匿名函数,多了一层嵌套。
是否有方法,可以写出更加简洁的代码呢?
现在将遇到的问题提炼一下:
我们希望sendData函数接收3个参数。
其中1个参数,是在调用async.auto之前传入
另外2个参数,是在async.auto执行中传入。
显而易见,这种问题可以使用高阶函数解决。
改写sendData函数:
var sendData = (res) => (callback, results) => { res.send(results['queryData']); }
改写async.auto的入参:
module.exports = (req, res) => { async.auto({ 'openDB': openDB, 'createCollection': ['openDB', createCollection], 'queryData': ['createCollection', queryData], 'sendData': ['queryData', sendData(res)] }, (err, results) => { console.log(err); }); }
问题解决,不再需要多写一个匿名函数。
但是对于每一个类似sendData的函数都需要如此处理,显得十分麻烦。
因此可以做一个批量封装的函数(将原函数批量进行柯里化):
var packageFuncs = (functions) => { var newFunctions = {}; typeof functions == 'object' && Object.keys(functions).forEach((key) => { if (typeof functions[key] == 'function') { //封装新函数 newFunctions[key] = (...params1) => (...params2) => { functions[key](...params2.concat(params1)); } } }); return newFunctions; }
封装所有需要的函数:
pacakageFuncs({
openDB,
createCollection,
queryData,
sendData
});
完整的代码如下(拆成3个模块):
common.js
module.exports.packageFuncs = (functions) => { var newFunctions = {}; typeof functions == 'object' && Object.keys(functions).forEach((key) => { if (typeof functions[key] == 'function') { //封装新函数 newFunctions[key] = (...params1) => (...params2) => { functions[key](...params2.concat(params1)); } } }); return newFunctions; }
functions.js
var packageFuncs = require('./common').packageFuncs; var openDB = (callback, results) => { //open db db.open((err, db) => { callback(err, db); } } var createCollection = (callback, results) => { //create collection results['openDB'].createCollection('myCollection', {safe:true}, (err, collection) => { callback(err, collection); }); } var queryData = (callback, results) => { //query data results['createCollection'].find().toArray((err,docs) => { callback(err, docs); }); } var sendData = (callback, results, res) => { res.send(results['queryData']); } module.exports = pacakageFuncs({ openDB, createCollection, queryData, sendData });
controller.js
var async = require('async'); var {openDB, createCollection, queryData, sendData} = require('./functions'); module.exports = (req, res) => { async.auto({ 'openDB': openDB, 'createCollection': ['openDB', createCollection], 'queryData': ['createCollection', queryData], 'sendData': ['queryData', sendData(res)] }, (err, results) => { console.log(err); }); }