原文:
对于在node这种异步框架下的编程,唯一的难题是:如何控制哪些函数顺序执行,哪些函数并行执行。node中并没有内置的控制方法,在这里我分享编写本站程序时用到的一些技巧。
并行VS顺序
在应用程序中通常有一些步骤必须在先前的操作得出结果之后才能运行。在平常的顺序执行的程序中这非常容易解决,因为每一部分都必须等待前一部分执行完毕才能执行。
Node中,除了那些执行阻塞IO的方法,其他方法都会存在这个问题。比如,扫描文件夹、打开文件、读取文件内容、查询数据库等等。
对于我的博客引擎,有一些以树形结构组织的文件需要处理。步骤如下所示:
- 获取文章列表 (译者注:作者的博客采取文件系统存储文章,获取文章列表,起始就是扫描文件夹)。
- 读入并解析文章数据。
- 获取作者列表。
- 读取并解析作者数据。
- 获取HAML模版列表。
- 读取所有HAML模版。
- 获取资源文件列表。
- 读取所有资源文件。
- 生成文章html页面。
- 生成作者页面。
- 生成索引页(index page)。
- 生成feed页。
- 生成静态资源文件。
如你所见,有些步骤可以不依赖其他步骤独立执行(但有些不行)。例如,我可以同时读取所有文件,但必须在扫描文件夹获取文件列表之后。我可以同时写入所有文件,但是必须等待文件内容都计算完毕才能写入。
使用分组计数器
对于如下这个扫猫文件夹并读取其中文件的例子,我们可以使用一个简单的计数器:
- var fs = require('fs');
- fs.readdir(".", function (err, files) {
- var count = files.length,
- results = {};
- files.forEach(function (filename) {
- fs.readFile(filename, function (data) {
- results[filename] = data;
- count--;
- if (count <= 0) {
- // Do something once we know all the files are read.
- }
- });
- });
- });
嵌套回调函数是保证它们顺序执行的好方法。所以在readdir回调函数中,我们根据文件数量设定了一个倒数计数器。然后我们对每个文件执行 readfile操作,它们将并行执行并以任意顺序完成。最重要的是,在每个文件读取完成时计数器的值会减小1,当它的值变为0的时候我们就知道文件全部读取完毕了。
通过传递回调函数避免过度嵌套
在取得文件内容之后,我们可以在最里层的函数中执行其他操作。但是当顺序操作超过7级之后,这将很快成为一个问题。
让我们使用传递回调的方式修改一下上面的实例:
- var fs = require('fs');
- function read_directory(path, next) {
- fs.readdir(".", function (err, files) {
- var count = files.length,
- results = {};
- files.forEach(function (filename) {
- fs.readFile(filename, function (data) {
- results[filename] = data;
- count--;
- if (count <= 0) {
- next(results);
- }
- });
- });
- });
- }
- function read_directories(paths, next) {
- var count = paths.length,
- data = {};
- paths.forEach(function (path) {
- read_directory(path, function (results) {
- data[path] = results;
- count--;
- if (count <= 0) {
- next(data);
- }
- });
- });
- }
- read_directories(['articles', 'authors', 'skin'], function (data) {
- // Do something
- });
现在我们写了一个混合的异步函数,它接收一些参数(本例中为路径),和一个在完成所有操作后调用的回调函数。所有的操作都将在回调函数中完成,最重要的是我们将多层嵌套转化为一个非嵌套的回调函数。
Combo库
我利用空闲时间编写了一个简单的Combo库。基本上,它封装了进行事件计数,并在所有事件完成之后调用回调函数的这个过程。同时它也保证不管回调函数的实际执行时间,都能保证它们按照注册的顺序执行。
- function Combo(callback) {
- this.callback = callback;
- this.items = 0;
- this.results = [];
- }
- Combo.prototype = {
- add: function () {
- var self = this,
- id = this.items;
- this.items++;
- return function () {
- self.check(id, arguments);
- };
- },
- check: function (id, arguments) {
- this.results[id] = Array.prototype.slice.call(arguments);
- this.items--;
- if (this.items == 0) {
- this.callback.apply(this, this.results);
- }
- }
- };
如果你想从数据库和文件中读取数据,并在完成之后执行一些操作,你可以如下进行:
- // Make a Combo object.
- var both = new Combo(function (db_result, file_contents) {
- // Do something
- });
- // Fire off the database query
- people.find({name: "Tim", age: 27}, both.add());
- // Fire off the file read
- fs.readFile('famous_quotes.txt', both.add());
数据库查询和文件读取将同时开始,当他们全部完成之后,传递给combo构造函数的回调函数将会被调用。第一个参数是数据库查询结果,第二个参数是文件读取结果。
结论
本篇文章中介绍的技巧:
- 通过嵌套回调,得到顺序执行的行为。
- 通过直接函数调用,得到并行执行的行为。
- 通过回调函数来化解顺序操作造成的嵌套。
- 使用计数器检测一组并行的操作什么时候完成。
- 使用类似combo这样的库来简化操作。