stepjs介绍--异步编程风格(2)

来源:互联网 发布:阿里云上线时间 编辑:程序博客网 时间:2024/06/06 09:28

Step.js(https://github.com/creationix/step)是控制流程工具(大小仅 150 行代码),解决回调嵌套层次过多等问题。适用于读文件、查询数据库等回调函数相互依赖,或者分别获取内容最后组合数据返回等应用情景。异步执行简单地可以分为“串行执行”和“并行”执行,下面我们分别去看看。

串行执行

这个库只有一个方法 Step(fns...)。Step 方法其参数 fns 允许是多个函数,这些函数被依次执行。Step 利用 this 对象指针来封装工作流,如下例:

[javascript] view plaincopy在CODE上查看代码片派生到我的代码片
  1. Step(  
  2.   function readSelf() {  
  3.     fs.readFile(__filename, this); // 把 this 送入 readFile 的异步参数中。此时 this 其类型为 function  
  4.     // 注意这里无须 return 任何值  
  5.   },  
  6.   function capitalize(err, text) { // err 为错误信息,如果有则抛出异常  
  7.     if (err) throw err;  
  8.     return text.toUpperCase();     // text 为 上个步骤 readFile 的值也就是文件内容。注意此处有返回值供下一步所用。  
  9.   },  
  10.   function showIt(err, newText) {  
  11.     if (err) throw err;  
  12.     console.log(newText);  
  13.   }  
  14. );  

可见,回调函数顺序依赖、依次执行,适用于下一个回调函数依赖于上一个函数执行的结果。Step 的一个约定,回调函数的第一个参数总是 err,第二个才是值(沿用 Node 回调的风格)。如果上一个步骤发生异常,那么异常对象将被送入到下一个步骤中。因此我们应该检查 err 是否有意义,如果 err 存在值,应抛出异常用于处理错误信息,中止下面执行下面的逻辑;如果 err 为 null 或者 undefined,则表示第二个参数才有意义。

为什么要大家注意有 return 和无return 的区别呢?因为这将要揭示 Step 的一个思想:只要是同步的工作,我们只需直接 return 即可。如果需要异步的话,则调用对象指针 this,此时 this 是一个函数类型的对象。

像上例一个 fn 接着一个 fn 执行,称为“串行执行”。

并行执行

所谓并行执行,就是等待所有回调产生的结果,并把所有结果按调用次序组成数组,作为参数传给最后一个函数处理。组装回调函数的结果,此方法不同于上面的依赖串联执行,所有的回调是并行执行的, 只不过是在最后一个回调返回结果时,才调用最终处理函数。

并行执行有点类似于 Promise 模式的 when 场景,要求所有条件为“并 AND”的逻辑关系时方执行。

[javascript] view plaincopy在CODE上查看代码片派生到我的代码片
  1. Step(  
  2.   // 同时执行两项任务 Loads two files in parallel  
  3.   function loadStuff() {  
  4.     fs.readFile(__filename, this.parallel());  
  5.     fs.readFile("/etc/passwd"this.parallel());  
  6.   },  
  7.   // 获取结果 Show the result when done  
  8.   function showStuff(err, code, users) {  
  9.     if (err) throw err;  
  10.     console.log(code);  
  11.     console.log(users);  
  12.   }  
  13. )  

我们用 Step.js API 提供的 this.parallel() 方法代替了 上一例的 this。this.parallel() 可以帮助解决我们上一例中只能产生一次异步函数的困窘,适合多个异步步骤的分发,最后完成阶段由最后一个回调收集结果数据。可见,调用了 n 次的 this.parallel(),就调用了 n 次的收集 results 的函数。如果任何一个异步分支发生异常,都会被收集到 results 中,并且特别地声明到 err 参数中,表示任务失败,最后一个回调不执行。

在两个步骤直接的值或者异常如何传递,我们可以总结一下三个场景:

  • 既不使用 this,又无 return 任何值,任务中断
  • 直接 return 值,此时无任何异步过程发生
  • 调用 this : Function,产生一个异步过程,此异步过程完成后,进入下一个 step
  • 用 this.parallel() : Function,产生多个异步过程,等待这些异步过程通通完成之后,才进入下一个 step

由此可见,Step.js 可以很好地结合同步与异步的任务组织技术。

并行执行_高阶

如果不确定多个异步任务的数量,可以使用 this.group()。

[javascript] view plaincopy在CODE上查看代码片派生到我的代码片
  1. Step(  
  2.   function readDir() {  
  3.     fs.readdir(__dirname, this);  
  4.   },  
  5.   function readFiles(err, results) {  
  6.     if (err) throw err;  
  7.     // 创建 group,其实内部创建一个 results 数组,保存结果  
  8.     var group = this.group();  
  9.     results.forEach(function (filename) {  
  10.       if (/\.js$/.test(filename)) {  
  11.         fs.readFile(__dirname + "/" + filename, 'utf8', group()); // group() 内部其逻辑与 this.parallel,也是调度 index/pending。  
  12.       }  
  13.     });  
  14.   },  
  15.   function showAll(err , files) {  
  16.     if (err) throw err;  
  17.     console.dir(files);  
  18.   }  
  19. );  

this.group 与 this.parallel 非常类似都是用于异步场景,至于区别的地方,在于 this.parallel 会把结果作为一个个单独的参数来提供,而 this.group 会将结果合并为数组 Array,表面上看,它们之间的区别有点象 fn.call() 与 fn.apply() 之间的区别,而按照使用场景的角度看,两者的真正区别是 thsi.group 用于不太确定异步任务数量的场景。

运用 this.group 又一例,等价的 map():

[javascript] view plaincopy在CODE上查看代码片派生到我的代码片
  1. function stepMAp(arr, iterator, callback){  
  2.   Step(  
  3.     function(){  
  4.   
  5.     var group = this.group();  
  6.     for(var i = 0, j = arr.length; i < j; i++)  
  7.       iterator(arr[i], group());  
  8.     }),  
  9.    callback  
  10. }  
  11. //  

源码解析

实际上 Step.js 的原理并不复杂,主要是递归函数来遍历函数列表。用计算器标记异步任务是否完成。比较巧妙的地方就是善用了对象指针 this。

Step.js 虽不如 Async.js(https://github.com/caolan/async) 提供诸多函数,但胜在够简单,用户可以自己继续封装新的函数来完善它。笔者认同这一观点,通过 Step.js 能鼓励我们透彻地思考问题并编写出优雅高效方案。

[javascript] view plaincopy在CODE上查看代码片派生到我的代码片
  1. // Inspired by http://github.com/willconant/flow-js, but reimplemented and  
  2. // modified to fit my taste and the node.JS error handling system.  
  3. function Step() {  
  4.   var steps = Array.prototype.slice.call(arguments), // 参数列表  
  5.       counter, pending, results, lock;               // 全局的四个变量  
  6.   
  7.   // 定义主函数 Define the main callback that's given as `this` to the steps.  
  8.   function next() {  
  9.     counter = pending = 0; // counter 和 pendig 是用于并行步骤任务:保存数据的索引和是否执行完毕的计数器  
  10.   
  11.     // 看看是否还有剩余的 steps Check if there are no steps left  
  12.     if (steps.length === 0) {  
  13.       // 抛出未捕获的异常 Throw uncaught errors  
  14.       if (arguments[0]) {  
  15.         throw arguments[0];  
  16.       }  
  17.       return;  
  18.     }  
  19.   
  20.     // 得到要执行的步骤 Get the next step to execute  
  21.     var fn = steps.shift();  
  22.     results = [];  
  23.   
  24.     // try...catch 捕获异常 Run the step in a try..catch block so exceptions don't get out of hand.  
  25.     try {  
  26.       lock = true;  
  27.       var result = fn.apply(next, arguments); // 此的 args 是 next 的擦参数列表  
  28.     } catch (e) {  
  29.       // 如果有异常,把捕捉到的异常送入下一个回调 Pass any exceptions on through the next callback  
  30.       next(e);  
  31.     }  
  32.   
  33.     if (counter > 0 && pending == 0) { // couter > 0 表示有并行任务,pending == 0 表示全部运行完毕  
  34.       // 如果执行了并行任务(异步的意思),而且全部分支都同步执行完毕后,立刻执行下一步  
  35.       // If parallel() was called, and all parallel branches executed  
  36.       // synchronously, go on to the next step immediately.  
  37.       next.apply(null, results); // 足以注意这里是 results 数组。此时 results 已经包含了全部的结果  
  38.     } else if (result !== undefined) {  
  39.       // 如发现有 return 执行(串行的意思),将其送入到回调 If a synchronous return is used, pass it to the callback  
  40.       next(undefined, result);  
  41.     }  
  42.     lock = false;  
  43.   }  
  44.   
  45.   // 用于并行的生成器 Add a special callback generator `this.parallel()` that groups stuff.  
  46.   next.parallel = function () {  
  47.     var index = 1 + counter++;  
  48.     pending++; // 开启了一个新的异步任务  
  49.   
  50.     return function () {  
  51.       pending--;// 计算器减 1,表示执行完毕  
  52.       // 如果有错误,则保存在结果数组的第一个元素中 Compress the error from any result to the first argument  
  53.       if (arguments[0]) {  
  54.         results[0] = arguments[0];  
  55.       }  
  56.       // 按次序保存结果 Send the other results as arguments  
  57.       results[index] = arguments[1];  
  58.       if (!lock && pending === 0) {// 最后才执行   
  59.         // 当所有分支都搞定,执行最后一个回调。When all parallel branches done, call the callback  
  60.         next.apply(null, results);  
  61.       }  
  62.     };  
  63.   };  
  64.   
  65.   // Generates a callback generator for grouped results  
  66.   next.group = function () {  
  67.     var localCallback = next.parallel();  
  68.     var counter = 0;  
  69.     var pending = 0;  
  70.     var result = [];  
  71.     var error = undefined;  
  72.   
  73.     function check() {  
  74.       if (pending === 0) {  
  75.         // When group is done, call the callback  
  76.         localCallback(error, result);  
  77.       }  
  78.     }  
  79.     process.nextTick(check); // Ensures that check is called at least once  
  80.   
  81.     // Generates a callback for the group  
  82.     return function () {  
  83.       var index = counter++;  
  84.       pending++;  
  85.       return function () {  
  86.         pending--;  
  87.         // Compress the error from any result to the first argument  
  88.         if (arguments[0]) {  
  89.           error = arguments[0];  
  90.         }  
  91.         // Send the other results as arguments  
  92.         result[index] = arguments[1];  
  93.         if (!lock) { check(); }  
  94.       };  
  95.     };  
  96.   };  
  97.   
  98.   // 开始工作流 Start the engine an pass nothing to the first step.  
  99.   next();  
  100. }  
  101.   
  102. /** 
  103.  
  104. 相当于一个包装步骤列表的工厂 
  105. */  
  106. // Tack on leading and tailing steps for input and output and return  
  107. // the whole thing as a function. Basically turns step calls into function  
  108. // factories.  
  109. Step.fn = function StepFn() {  
  110.   var steps = Array.prototype.slice.call(arguments);  
  111.   return function () {  
  112.     var args = Array.prototype.slice.call(arguments);  
  113.   
  114.     // Insert a first step that primes the data stream  
  115.     var toRun = [function () {  
  116.       this.apply(null, args);  
  117.     }].concat(steps);  
  118.   
  119.     // 加入最后的步骤 If the last arg is a function add it as a last step  
  120.     if (typeof args[args.length-1] === 'function') {  
  121.       toRun.push(args.pop());  
  122.     }  
  123.   
  124.   
  125.     Step.apply(null, toRun);  
  126.   }  
  127. }  
  128.   
  129.   
  130. // CommonJS模块系统的钩子 Hook into commonJS module systems  
  131. if (typeof module !== 'undefined' && "exports" in module) {  
  132.   module.exports = Step;  
  133. }  

我为适应 sea.js 包机制,加入define() 调用,见:http://naturaljs.googlecode.com/svn/trunk/libs/step.js

原来 Step.js 也是参考了别人的代码,https://github.com/willconant/flow-js,站在巨人的肩膀上啊。参见同类型的开源库:

  • https://github.com/isaacs/slide-flow-control
  • 国产 Step.js:https://github.com/myworld4059/Step.js/blob/master/Step.js
转自:http://blog.csdn.net/zhangxin09/article/details/13018739
0 0