说起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);
    });
}