浅析Node.js中的流程控制

对于在node这种异步框架下的编程,唯一的难题是:如何控制哪些函数顺序执行,哪些函数并行执行。node中并没有内置的控制方法,在这里我分享编写本站程序时用到的一些技巧。

并行VS顺序

在应用程序中通常有一些步骤必须在先前的操作得出结果之后才能运行。在平常的顺序执行的程序中这非常容易解决,因为每一部分都必须等待前一部分执行完毕才能执行。

Node中,除了那些执行阻塞IO的方法,其他方法都会存在这个问题。比如,扫描文件夹、打开文件、读取文件内容、查询数据库等等。

对于我的博客引擎,有一些以树形结构组织的文件需要处理。步骤如下所示:

◆ 获取文章列表 (译者注:作者的博客采取文件系统存储文章,获取文章列表,起始就是扫描文件夹)。

◆ 读入并解析文章数据。

◆ 获取作者列表。

◆ 读取并解析作者数据。

◆ 获取HAML模版列表。

◆ 读取所有HAML模版。

◆ 获取资源文件列表。

◆ 读取所有资源文件。

◆ 生成文章html页面。

◆ 生成作者页面。

◆ 生成索引页(index page)。

◆ 生成feed页。

◆ 生成静态资源文件。

如你所见,有些步骤可以不依赖其他步骤独立执行(但有些不行)。例如,我可以同时读取所有文件,但必须在扫描文件夹获取文件列表之后。我可以同时写入所有文件,但是必须等待文件内容都计算完毕才能写入。

使用分组计数器

对于如下这个扫猫文件夹并读取其中文件的例子,我们可以使用一个简单的计数器:

 
 
 
  1. var fs = require('fs');  
  2.  
  3. fs.readdir(".", function (err, files) {  
  4.   var count = files.length,  
  5.       results = {};  
  6.   files.forEach(function (filename) {  
  7.     fs.readFile(filename, function (data) {  
  8.       results[filename] = data;  
  9.       count--;  
  10.       if (count <= 0) {  
  11.         // Do something once we know all the files are read.  
  12.       }  
  13.     });  
  14.   });  
  15. });  

嵌套回调函数是保证它们顺序执行的好方法。所以在readdir回调函数中,我们根据文件数量设定了一个倒数计数器。然后我们对每个文件执行readfile操作,它们将并行执行并以任意顺序完成。最重要的是,在每个文件读取完成时计数器的值会减小1,当它的值变为0的时候我们就知道文件全部读取完毕了。

通过传递回调函数避免过度嵌套

在取得文件内容之后,我们可以在最里层的函数中执行其他操作。但是当顺序操作超过7级之后,这将很快成为一个问题。

让我们使用传递回调的方式修改一下上面的实例:

 
 
 
  1. var fs = require('fs');  
  2.  
  3. function read_directory(path, next) {  
  4.   fs.readdir(".", function (err, files) {  
  5.     var count = files.length,  
  6.         results = {};  
  7.     files.forEach(function (filename) {  
  8.       fs.readFile(filename, function (data) {  
  9.         results[filename] = data;  
  10.         count--;  
  11.         if (count <= 0) {  
  12.           next(results);  
  13.         }  
  14.       });  
  15.     });  
  16.   });  
  17. }  
  18.  
  19. function read_directories(paths, next) {  
  20.   var count = paths.length,  
  21.       data = {};  
  22.   paths.forEach(function (path) {  
  23.     read_directory(path, function (results) {  
  24.       data[path] = results;  
  25.       count--;  
  26.       if (count <= 0) {  
  27.         next(data);  
  28.       }  
  29.     });  
  30.   });  
  31. }  
  32.  
  33. read_directories(['articles', 'authors', 'skin'], function (data) {  
  34.   // Do something  
  35. });  

现在我们写了一个混合的异步函数,它接收一些参数(本例中为路径),和一个在完成所有操作后调用的回调函数。所有的操作都将在回调函数中完成,最重要的是我们将多层嵌套转化为一个非嵌套的回调函数。

Combo库

我利用空闲时间编写了一个简单的Combo库。基本上,它封装了进行事件计数,并在所有事件完成之后调用回调函数的这个过程。同时它也保证不管回调函数的实际执行时间,都能保证它们按照注册的顺序执行。

 
 
 
  1. function Combo(callback) {  
  2.   this.callback = callback;  
  3.   this.items = 0;  
  4.   this.results = [];  
  5. }  
  6.  
  7. Combo.prototype = {  
  8.   add: function () {  
  9.     var self = this,  
  10.         id = this.items;  
  11.     this.items++;  
  12.     return function () {  
  13.       self.check(id, arguments);  
  14.     };  
  15.   },  
  16.   check: function (id, arguments) {  
  17.     this.results[id] = Array.prototype.slice.call(arguments);  
  18.     this.items--;  
  19.     if (this.items == 0) {  
  20.       this.callback.apply(this, this.results);  
  21.     }  
  22.   }  
  23. };  

如果你想从数据库和文件中读取数据,并在完成之后执行一些操作,你可以如下进行:

 
 
 
  1. // Make a Combo object.  
  2. var both = new Combo(function (db_result, file_contents) {  
  3.   // Do something  
  4. });  
  5. // Fire off the database query  
  6. people.find({name: "Tim", age: 27}, both.add());  
  7. // Fire off the file read  
  8. fs.readFile('famous_quotes.txt', both.add());  

数据库查询和文件读取将同时开始,当他们全部完成之后,传递给combo构造函数的回调函数将会被调用。第一个参数是数据库查询结果,第二个参数是文件读取结果。

结论

本篇文章中介绍的技巧:

◆ 通过嵌套回调,得到顺序执行的行为。

◆ 通过直接函数调用,得到并行执行的行为。

◆ 通过回调函数来化解顺序操作造成的嵌套。

◆ 使用计数器检测一组并行的操作什么时候完成。

◆ 使用类似combo这样的库来简化操作。

#p#

上一篇介绍流程控制的文章给我带来了很大的乐趣,现在我想要处理一些反馈,另外还要讨论一下inimino所作的伟大工作。

当前node中有两种处理异步返回值的方法:promises和event emitters。关于两种方法的细节,你可以阅读nodejs.org上的介绍。我将会讨论这两种方法和另一种处理异步返回值和流事件(streaming events)的方法。

为什么要区分Promise和EventEmitter?

在node中有两种处理事件的类,它们是:Promise和EventEmitter。Promise是函数的异步表现形式。

 
 
 
  1. var File = require('file');  
  2. var promise = File.read('mydata.txt');  
  3. promise.addCallback(function (text) {  
  4.   // Do something  
  5. });  
  6. promise.addErrback(function (err) {  
  7.   // Handle error  
  8. })  

File.read接受文件名并返回文件内容。

有时我们需要监听可能多次发生的事件。例如,在一个web服务中,处理web请求时,body事件多次被触发,然后complete事件被触发。

 
 
 
  1. Http.createServer(function (req, res) {  
  2.   var body = "";  
  3.   req.addListener('body', function (chunk) {  
  4.     body += chunk;  
  5.   });  
  6.   req.addListener('complete', function () {  
  7.     // Do something with body text  
  8.   });  
  9. }).listen(8080);  

这两种方式的不同之处在于:在使用promise时,你会得到success事件或者error事件,但不会同时得到,也不会得到一个以上事件。在处理会发生多次的事件的时候,你就需要更强大的 EventEmitters。

创建自定义promise

假定我想为posix.open, posix.write, 和posix.close写一个便于使用的包装函数filewrite。(如下代码摘自”file”函数库中File.write函数的真实代码)

 
 
 
  1. function fileWrite (filename, data) {  
  2.   var promise = new events.Promise();  
  3.   posix.open(filename, "w", 0666)  
  4.     .addCallback(function (fd) {  
  5.       function doWrite (_data) {  
  6.         posix.write(fd, _data, 0, encoding)  
  7.           .addCallback(function (written) {  
  8.             if (written === _data.length) {  
  9.               posix.close(fd);  
  10.               promise.emitSuccess();  
  11.             } else {  
  12.               doWrite(_data.slice(written));  
  13.             }  
  14.           }).addErrback(function () {  
  15.             promise.emitError();  
  16.           });  
  17.       }  
  18.       doWrite(data);  
  19.     })  
  20.     .addErrback(function () {  
  21.       promise.emitError();  
  22.     });  
  23.   return promise;  
  24. };  

filewrite函数可以以如下形式使用:

 
 
 
  1. fileWrite("MyBlog.txt", "Hello World").addCallback(function () {  
  2.   // It's done  
  3. });  

请注意,我必须创建一个promise对象,执行操作,然后将结果传递给这个promise对象。

还有更好的方法

promises工作良好,但是继续读过inimino之后,它所使用的方法令我印象深刻。

是否还记得我们的第一个例子?假设我们按照如下方式使用File.read:

 
 
 
  1. var File = require('file');  
  2. File.read('mydata.txt')(function (text) {  
  3.   // Do something  
  4. }, function (error) {  
  5.   // Handle error  
  6. });  

它不返回promise对象,而是返回一个接受两个回调函数作为参数的函数:一个处理成功,一个处理失败。我把这种风格成为“Do风格”,下面我详细解释:

编写回调风格的代码

如果我们想定义一个不立刻返回值的函数。使用”Do”风格,filewirte函数应当如下使用:(假定之前提到的posix函数也是这个风格)

 
 
 
  1. function fileWrite (filename, data) { return function (callback, errback) {  
  2.   posix.open(filename, "w", 0666)(function (fd) {  
  3.     function doWrite (_data) {  
  4.       posix.write(fd, _data, 0, encoding)(  
  5.         function (written) {  
  6.           if (written === _data.length) {  
  7.             posix.close(fd);  
  8.             callback();  
  9.           } else {  
  10.             doWrite(_data.slice(written));  
  11.           }  
  12.         }, errback);  
  13.     }  
  14.     doWrite(data);  
  15.   }, errback);  
  16. }};  

请注意,这样很容易就把错误信息返回给了调用者。同时,这种风格也使代码更短,更易阅读。

使用这种风格编写代码的关键是:不要返回promise,而是返回一个接受两个回调的函数,在需要的时候直接调用返回的函数。

“Do”函数库

前些日子我写了一个小型的函数库,叫做“Do”。实际上,它只有一个执行并行操作的函数,就像上一篇文章中介绍的“Combo”库。

实现

如下是整个函数的实现:

 
 
 
  1. Do = {  
  2.   parallel: function (fns) {  
  3.     var results = [],  
  4.         counter = fns.length;  
  5.     return function(callback, errback) {  
  6.       fns.forEach(function (fn, i) {  
  7.         fn(function (result) {  
  8.           results[i] = result;  
  9.           counter--;  
  10.           if (counter <= 0) {  
  11.             callback.apply(null, results);  
  12.           }  
  13.         }, errback);  
  14.       });  
  15.     }  
  16.   }  
  17. };  

结合回调风格,使用这个函数可以写出非常强大和简介的代码。

执行单个操作

我们假定有一个实现了这个新技巧的函数readFIle,可以如下使用这个函数:

 
 
 
  1. // A single async action with error handling  
  2. readFile('secretplans.txt')(function (secrets) {  
  3.   // Do something  
  4. }, function (error) {  
  5.   // Handle Error  
  6. });  

执行并行操作

我们继续使用”Do"函数库

 
 
 
  1. Do.parallel([  
  2.     readFile('mylib.js'),  
  3.     readFile('secretplans.txt'),  
  4. ])(function (source, secrets) {  
  5.   // Do something  
  6. }, function (error) {  
  7.   // Handle Error  
  8. });  

上述代码代码并行执行了两个异步操作,并在全部执行完毕后执行指定代码。注意,如果没有错误发生,只有处理success的回调函数会被执行。如果出错,函数会将错误传递给通常的错误处理代码。

你也可传递一个文件名数组。

 
 
 
  1. var files = ["one.txt", "two.txt", "three.txt"];  
  2. var actions = files.map(function (filename) {  
  3.   return readFile(filename);  
  4. });  
  5.  
  6. Do.parallel(actions)(function () {  
  7.   var contents = {},  
  8.       args = arguments;  
  9.   files.forEach(function (filename, index) {  
  10.     contents[filename] = args[index];  
  11.   });  
  12.   // Do something  
  13. });  
  14. // Let error thow exception.  

执行顺序操作

要执行顺序操作,只需将函数“串起来”即可:

 
 
 
  1. readFile('names.txt')(  
  2.   function upcase_slowly(string) { return function (next) {  
  3.     setTimeout(function () {  
  4.       next(string.toUpperCase());  
  5.     }, 100);  
  6.   }}  
  7. )(  
  8.   function save_data(string) { return function (next) {  
  9.     writeFile('names_up.txt', string)(next);  
  10.   }}  
  11. )(function () {  
  12.   // File was saved  
  13. });  

上述代码读取文件'names.txt',完成之后调用upcase_slowly,然后将生成的新字符串专递给save_data函数。save_data函数是对writeFile的一个包装。当save_data函数执行完毕之后,将执行最终的回调函数。

Just for fun, here is the same example translated to the Jack language (still in development).

开个玩笑,如下代码是翻译成Jack语言(还在开发中)的示例代码:

 
 
 
  1. readFile names.txt  
  2. | fun string -> next ->  
  3.   timeout 100, fun ->  
  4.     next string.toUpperCase()  
  5. | fun string -> next ->  
  6.   writeFile 'names_up.txt', string | next  
  7. | fun ->  
  8.  # File was saved  

原文:http://www.grati.org/?cat=35

【编辑推荐】

  1. 揭秘Node.js事件
  2. Node.js初探之hello world
  3. Node.js初探之与Mysql的交互
  4. Node.js入门之神秘的服务器端JavaScript
  5. 什么是Node.js?

网页标题:浅析Node.js中的流程控制
当前路径:http://www.shufengxianlan.com/qtweb/news48/331698.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联