简化循环和闭包中的嵌套承诺

时间:2017-05-19 10:43:11

标签: javascript loops promise nested closures

我写了一个~50行脚本来对MySQL数据库进行内务处理。我担心我的代码会出现反模式,因为它会迅速升级为无法读取的简单函数。

我想提高一些提高可读性的意见 完整的脚本位于本文的底部,以提供一个想法。

聚焦问题

过度嵌套是由于这样的模式一再重复引起的:(从脚本中获取的片段)

sql.query("show databases")
.then(function(rows) {
    for (var r of rows) {
        var db = r.Database;

        (function(db) {
            sql.query("show tables in " + db)
            .then(function(rows) {
            // [...]
            }
        })(db);
    }
});

我在for循环和闭包中嵌套了一个承诺。循环需要迭代来自sql.query()的所有结果,并且必须使用闭包将db的值传递给较低的promise;如果没有闭包,循环甚至会在嵌套的promise执行之前完成,因此db将始终只包含循环的最后一个元素,从而阻止嵌套的promise承诺读取db的每个值。 / p>

完整脚本

var mysql     = require("promise-mysql");
var validator = require("mysql-validator");  // simple library to validate against mysql data types

var ignoreDbs  = [ "information_schema" ],
    multiplier = 2,  // numeric records multiplier to check out-of-range proximity
    exitStatus = {'ok': 0, 'nearOutOfRange': 1, 'systemError': 2};

(function() {
    var sql,

        mysqlHost = "localhost",
        mysqlUser = "user",
        mysqlPass = "";

    mysql.createConnection({
        host:     mysqlHost,
        user:     mysqlUser,
        password: mysqlPass
    }).then(function(connection) {
        sql = connection;
    })

   .then(function() {
    sql.query("show databases")
    .then(function(rows) {
        for (var r of rows) {
        var db = r.Database;

        if (ignoreDbs.indexOf(db) != -1) continue;
        (function(db) {
            sql.query("show tables in " + db)
            .then(function(rows) {
            for (var r of rows) {
                var table = r["Tables_in_" + db];

                (function(table) {
                sql.query("describe " + db + "." + table)
                    .then(function(rows) {
                    for (var r of rows) {
                    (function(r) {
                        var field = r.Field,
                            type  = r.Type,   // eg: decimal(10,2)
                                                query = "select " + field + " from " + db + "." + table + " ";

                        if (table != "nonce") query += "order by date desc limit 1000";

                        sql.query(query)
                        .then(function(rows) {
                        for (var r of rows) {
                            var record, err;

                            // remove decimal part, only integer range is checked
                            record = Math.trunc(r[field]);
                            err = validator.check(record * multiplier, type);
                            if (err) {
                            console.log(err.message);
                            process.exit(exitStatus.nearOutOfRange);
                            }
                        }
                        });
                    })(r);
                    }
                });
                })(table);
            }
            });
        })(db);
        }
    });
    })
    .then(function() {
    // if (sql != null) sql.end();  // may not exit process here: sql connection terminates before async functions above
    //process.exit(exitStatus.ok);  //
    });
})();

琐事

该脚本的目的是自动并定期监视存储在MySQL中任何行,表和数据库中的任何记录是否接近其特定数据类型的超出范围限制。连接到MySQL的其他几个进程不断地插入具有增加值和随机数的新数值数据;此脚本是检查此类数字限制的中心点。然后将该脚本附加到Munin以进行持续监控和警报。

更新:修改后的脚本

正如@Kqcef所建议的那样,我将来自promise nest的匿名函数模块化,并使用let来避免显式嵌套附加函数以保留变量上下文。

这仍然过于冗长,之前我在Bash中用大约40行编写了相同的脚本,但是对于一个端口到nodejs,性能却在尖叫。

"use strict";

var mysql     = require("promise-mysql");
var validator = require("mysql-validator");  // a simple library to validate against mysql data types

var ignoreDbs  = [ "information_schema" ],
    multiplier = 2,  // numeric records multiplier to check out-of-range proximity
    exitStatus = {'ok': 0, 'nearOutOfRange': 1, 'systemError': 2};

var mysqlHost = "localhost",
    mysqlUser = "btc",
    mysqlPass = "";

// return array of DBs strings
function getDatabases(sql) {
    return sql.query("show databases")
    .then(function(rows) {
        var dbs = [];

        for (var r of rows)
            dbs.push(r.Database);

        return dbs;
    });
}

// return array of tables strings
function getTables(sql, db) {
    return sql.query("show tables in " + db)
    .then(function(rows) {
        var tables = [];

        for (var r of rows)
            tables.push(r["Tables_in_" + db]);

        return tables;
    });
}

// return array of descriptions
function getTableDescription(sql, db, table) {
    return sql.query("describe " + db + "." + table)
    .then(function(rows) {
        var descrs = [];

        for (var r of rows) {
            descrs.push({ 'field': r.Field,    // eg: price
                          'type':  r.Type});   // eg: decimal(10,2)
        }

        return descrs;
    });
}

// return err object
function validateRecord(record, type) {
    var record, err;

    if (typeof record != "number") {
        console.log("error: record is not numeric.");
        process.exit(exitStatus.systemError);
    }

    // remove decimal part, only integer range is checked
    record = Math.trunc(record);
    err = validator.check(record * multiplier, type);

    return err;
}

(function() {
    var sql;

    mysql.createConnection({
        host:     mysqlHost,
        user:     mysqlUser,
        password: mysqlPass
    }).then(function(connection) {
        sql = connection;
    })

    .then(function() {
        return getDatabases(sql)
    })
    .then(function(dbs) {
        dbs.forEach(function(db) {
            if (ignoreDbs.indexOf(db) != -1) return;
            getTables(sql, db)
            .then(function(tables) {
                tables.forEach(function(table) {
                    getTableDescription(sql, db, table)
                    .then(function(descrs) {
                        descrs.forEach(function(descr) {
                            let field = descr.field,
                                type  = descr.type,
                                query = "select " + descr.field + " from " + db + "." + table + " ";

                            if (table != "nonce") query += "order by date desc limit 1000";

                            sql.query(query)
                            .then(function(rows) {
                                rows.forEach(function(row) {
                                    let err = validateRecord(row[field], type);
                                    if (err) {
                                        console.log(err.message);
                                        process.exit(exitStatus.nearOutOfRange);
                                    }
                                });
                            });
                        });
                    });
                });
            });
        });
    });


/*
    .then(function() {
        //if (sql != null) sql.end();
        //process.exit(exitStatus.ok);
    });
*/
})();

1 个答案:

答案 0 :(得分:2)

我同意Jaromanda在你的for循环中使用function getTableDescription(sql, db, table) { return sql.query("describe " + db + "." + table) .then(function(rows) { var descrs = []; for (var r of rows) { descrs.push({ 'field': r.Field, // eg: price 'type': r.Type}); // eg: decimal(10,2) } return descrs; }); } 来阻止值的范围并避免使用一个立即调用的函数,虽然在功能方面完全正确,但它的可读性明显较低

在最佳实践和避免反模式方面,您可以在写作和“好”方面努力做到最重要的事情之一。代码正在构建模块化,可重用的代码块。就目前而言,您的代码具有5或6个匿名函数,这些函数在您的承诺回调链中无处可用。如果你要将它们声明为该链之外的函数,那么这不仅会提高代码的可维护性(你可以测试每个代码),但是,如果它们的名字清楚地表明了它们的用途,那么它将具有可读性承诺链。

(根据用户问题更新)

而不是留下内部功能......

function collectDescriptionsFromRows(rows) {
  var descriptions = [];
  for (var row of rows) {
    descriptions.push({'field': row.Field, 'type': row.Type});
  }
  return descriptions;
}

function getTableDescription(sql, db, table) {
    return sql.query("describe " + db + "." + table)
    .then(collectDescriptionsFromRows);
}

...您可以轻松地将其删除,以便您的代码能够自我记录:

collectDescriptionsFromRows

此外,如果您发现自己正在从一个阵列到另一个阵列进行数据收集,那么习惯使用内置的高阶函数(map,filter,reduce)会非常有帮助。而不是我刚刚列出的function collectDescriptionsFromRows(rows) { return rows.map(row => { 'field': row.Field, 'type': row.Type}); } ,它可以简化为:

function(

更简洁,更具可读性。如果您继续在链中提取这些匿名函数,那么您的代码和承诺链将缩小和读取更像是一步一步的指令列表。你看到interface SearchEngineSelector{ URL getSearchEngineUrl(); } 的任何地方......还有更多的提取要做!您还可以通过提取您需要的所有数据并使用本地逻辑将其归结为您所需的数据(而不是进行多次查询)来做一些损害(肯定)。希望这会有所帮助。