由于JavaScript语言异步特性,在使用Node.js执行很多操作时都会使用到回调函数,其中就包括访问数据库。如果代码中的业务逻辑稍微复杂一点,回调一层层嵌套,那么代码很容易进入Callback Hell,无论对写代码的人还是阅读代码的人,都是精神上的折磨。
例如对MySQL的一个事务操作,插入一条posts并插入一条log:
var title = 'It is a new post';
connection.beginTransaction(function(err) {
if (err) { throw err; }
connection.query('INSERT INTO posts SET title=?', title, function(err, result) {
if (err) {
return connection.rollback(function() {
throw err;
});
}
var log = 'Post ' + result.insertId + ' added';
connection.query('INSERT INTO log SET data=?', log, function(err, result) {
if (err) {
return connection.rollback(function() {
throw err;
});
}
connection.commit(function(err) {
if (err) {
return connection.rollback(function() {
throw err;
});
}
console.log('success!');
});
});
});
});
以上非常简单的一个业务逻辑,已经回调了好几层了,如果稍微再复杂一点,那么代码恐怕就无法直视了。
为了防止发生多层嵌套回调的大坑,可以使用Async.js来解决这个问题。下面来介绍Async.js结合操作MySQL数据库的使用。
假设需求是向log表中插入多条数据,最终返回执行结果,可以使用async.each函数:
var sqls = [
"INSERT INTO log SET data='data1'",
"INSERT INTO log SET data='data2'",
"INSERT INTO log SET data='data3'"
];
async.each(sqls, function(item, callback) {
// 遍历每条SQL并执行
connection.query(item, function(err, results) {
if(err) {
// 异常后调用callback并传入err
callback(err);
} else {
console.log(item + "执行成功");
// 执行完成后也要调用callback,不需要参数
callback();
}
});
}, function(err) {
// 所有SQL执行完成后回调
if(err) {
console.log(err);
} else {
console.log("SQL全部执行成功");
}
});
async.each并不能保证执行成功一条SQL语句后再去执行下一条,所以如果有一条执行失败,不会影响到其他语句的执行。
如果想要实现执行成功上一条语句后再开始执行数组中下一条语句,可以使用eachSeries函数:
var sqls = [
"INSERT INTO log SET data='data1'",
"INSERT INTO log SET data='data2'",
"INSERT INTO log SET data='data3'"
];
async.eachSeries(sqls, function(item, callback) {
// 遍历每条SQL并执行
connection.query(item, function(err, results) {
if(err) {
// 异常后调用callback并传入err
callback(err);
} else {
console.log(item + "执行成功");
// 执行完成后也要调用callback,不需要参数
callback();
}
});
}, function(err) {
// 所有SQL执行完成后回调
if(err) {
console.log(err);
} else {
console.log("SQL全部执行成功");
}
});
async.eachSeries保证了SQL的执行顺序,而且当其中一条执行异常,就不会继续执行下一条。
async.forEachOf类似于async.each,区别是可以接收Object类型参数,并且会在第二个参数回调函数中传入遍历到的每一项的key,更适合批量执行查询语句并返回结果:
var sqls = {
table_a: "select count(*) from table_a",
table_b: "select count(*) from table_b",
table_c: "select count(*) from table_c"
};
// 用于存放查询结果
var counts = {};
async.forEachOf(sqls, function(value, key, callback) {
// 遍历每条SQL并执行
connection.query(value, function(err, results) {
if(err) {
callback(err);
} else {
counts[key] = results[0]['count(*)'];
callback();
}
});
}, function(err) {
// 所有SQL执行完成后回调
if(err) {
console.log(err);
} else {
console.log(counts);
}
});
运行结果:
{ table_a: 26, table_b: 3, table_c: 2 }
上面的async.forEachOf获取多条Select语句的查询结果的代码可以使用async.map函数简化成这样:
var sqls = {
table_a: "select count(*) from table_a",
table_b: "select count(*) from table_b",
table_c: "select count(*) from table_c"
};
async.map(sqls, function(item, callback) {
connection.query(item, function(err, results) {
callback(err, results[0]['count(*)']);
});
}, function(err, results) {
if(err) {
console.log(err);
} else {
console.log(results);
}
});
运行结果:
{ table_a: 26, table_b: 3, table_c: 2 }
Async.js非常实用的一个功能就是流程控制。回到本文刚开始的那个开启事务执行Insert的例子,每一步都需要上一步执行成功后才能执行,很容易掉进回调大坑中。下面实用async.series函数来优化流程控制,让代码更优雅:
var title = 'It is a new post';
// 用于在posts插入成功后保存自动生成的ID
var postId = null;
// function数组,需要执行的任务列表,每个function都有一个参数callback函数并且要调用
var tasks = [function(callback) {
// 开启事务
connection.beginTransaction(function(err) {
callback(err);
});
}, function(callback) {
// 插入posts
connection.query('INSERT INTO posts SET title=?', title, function(err, result) {
postId = result.insertId;
callback(err);
});
}, function(callback) {
// 插入log
var log = 'Post ' + postId + ' added';
connection.query('INSERT INTO log SET data=?', log, function(err, result) {
callback(err);
});
}, function(callback) {
// 提交事务
connection.commit(function(err) {
callback(err);
});
}];
async.series(tasks, function(err, results) {
if(err) {
console.log(err);
connection.rollback(); // 发生错误事务回滚
}
connection.end();
});
上面使用async.series按顺序执行多条任务,但是很多情况下执行一个任务的时候需要用到上一条任务的相关数据,例如插入一条数据到posts表后,会自动生成ID,下一步插入日志会用到这个ID,如果使用async.series函数就需要定义一个变量var postId
来存储这个ID,此时可使用async.waterfall来替代async.series。
var title = 'It is a new post';
var tasks = [function(callback) {
connection.beginTransaction(function(err) {
callback(err);
});
}, function(callback) {
connection.query('INSERT INTO posts SET title=?', title, function(err, result) {
callback(err, result.insertId); // 生成的ID会传给下一个任务
});
}, function(insertId, callback) {
// 接收到上一条任务生成的ID
var log = 'Post ' + insertId + ' added';
connection.query('INSERT INTO log SET data=?', log, function(err, result) {
callback(err);
});
}, function(callback) {
connection.commit(function(err) {
callback(err);
});
}];
async.waterfall(tasks, function(err, results) {
if(err) {
console.log(err);
connection.rollback(); // 发生错误事务回滚
}
connection.end();
});
// tasks是一个Object
var tasks = {
table_a: function(callback) {
connection.query('select count(*) from table_a', function(err, result) {
callback(err, result[0]['count(*)']); // 将结果传入callback
});
},
table_b: function(callback) {
connection.query('select count(*) from table_b', function(err, result) {
callback(err, result[0]['count(*)']);
});
},
table_c: function(callback) {
connection.query('select count(*) from table_c', function (err, result) {
callback(err, result[0]['count(*)']);
});
}
};
async.series(tasks, function(err, results) {
if(err) {
console.log(err);
} else {
console.log(results);
}
connection.end();
});
运行结果:
{ table_a: 26, table_b: 3, table_c: 2 }
以上是Async.js操作数据库经常会用到的一些例子,用上它的话就不再需要担心异步回调的坑了。Async.js不仅仅可以用于数据库操作,其他用到异步回调函数的地方都可以使用,例如文件读写等,本文只是通过数据库操作为例来介绍Async.js基本用法,它同样也可以运用到其他需要它的地方。除了上面介绍的几个函数外,Async.js还提供了一些其他实用的函数,可以参考文档灵活使用。