Step.js 使用教程(附源码解析)

简介: Step.js(https://github.com/creationix/step)是控制流程工具(大小仅 150 行代码),解决回调嵌套层次过多等问题。适用于读文件、查询数据库等回调函数相互依赖,或者分别获取内容最后组合数据返回等应用情景。

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

串行执行

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

Step(
  function readSelf() {
    fs.readFile(__filename, this); // 把 this 送入 readFile 的异步参数中。此时 this 其类型为 function
    // 注意这里无须 return 任何值
  },
  function capitalize(err, text) { // err 为错误信息,如果有则抛出异常
    if (err) throw err;
    return text.toUpperCase();     // text 为 上个步骤 readFile 的值也就是文件内容。注意此处有返回值供下一步所用。
  },
  function showIt(err, newText) {
    if (err) throw err;
    console.log(newText);
  }
);

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

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

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

并行执行

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

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

Step(
  // 同时执行两项任务 Loads two files in parallel
  function loadStuff() {
    fs.readFile(__filename, this.parallel());
    fs.readFile("/etc/passwd", this.parallel());
  },
  // 获取结果 Show the result when done
  function showStuff(err, code, users) {
    if (err) throw err;
    console.log(code);
    console.log(users);
  }
)

我们用 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()。

Step(
  function readDir() {
    fs.readdir(__dirname, this);
  },
  function readFiles(err, results) {
    if (err) throw err;
    // 创建 group,其实内部创建一个 results 数组,保存结果
    var group = this.group();
    results.forEach(function (filename) {
      if (/\.js$/.test(filename)) {
        fs.readFile(__dirname + "/" + filename, 'utf8', group()); // group() 内部其逻辑与 this.parallel,也是调度 index/pending。
      }
    });
  },
  function showAll(err , files) {
    if (err) throw err;
    console.dir(files);
  }
);

this.group 与 this.parallel 非常类似都是用于异步场景,至于区别的地方,在于 this.parallel 会把结果作为一个个单独的参数来提供,而 this.group 会将结果合并为数组 Array。所以从表面上看它们之间的区别有点象 fn.call() 与 fn.apply() 之间的区别,——不知道大家有没有这种感受;而按照使用场景的角度看,两者的真正区别是 thsi.group 用于不太确定异步任务数量的场景。运用 this.group 又一例,等价的 map():

function stepMAp(arr, iterator, callback){
  Step(
    function(){

    var group = this.group();
    for(var i = 0, j = arr.length; i < j; i++)
      iterator(arr[i], group());
    }),
   callback
}
//

另外注意 group() 不宜直接调用,如 group()(args),这样会终止流程。

源码解析

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

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

// Inspired by http://github.com/willconant/flow-js, but reimplemented and
// modified to fit my taste and the node.JS error handling system.
function Step() {
  var steps = Array.prototype.slice.call(arguments), // 参数列表
      counter, pending, results, lock;               // 全局的四个变量

  // 定义主函数 Define the main callback that's given as `this` to the steps.
  function next() {
    counter = pending = 0; // counter 和 pendig 是用于并行步骤任务:保存数据的索引和是否执行完毕的计数器

    // 看看是否还有剩余的 steps Check if there are no steps left
    if (steps.length === 0) {
      // 抛出未捕获的异常 Throw uncaught errors
      if (arguments[0]) {
        throw arguments[0];
      }
      return;
    }

    // 得到要执行的步骤 Get the next step to execute
    var fn = steps.shift();
    results = [];

    // try...catch 捕获异常 Run the step in a try..catch block so exceptions don't get out of hand.
    try {
      lock = true;
      var result = fn.apply(next, arguments); // 此的 args 是 next 的擦参数列表
    } catch (e) {
      // 如果有异常,把捕捉到的异常送入下一个回调 Pass any exceptions on through the next callback
      next(e);
    }

    if (counter > 0 && pending == 0) { // couter > 0 表示有并行任务,pending == 0 表示全部运行完毕
      // 如果执行了并行任务(异步的意思),而且全部分支都同步执行完毕后,立刻执行下一步
      // If parallel() was called, and all parallel branches executed
      // synchronously, go on to the next step immediately.
      next.apply(null, results); // 足以注意这里是 results 数组。此时 results 已经包含了全部的结果
    } else if (result !== undefined) {
      // 如发现有 return 执行(串行的意思),将其送入到回调 If a synchronous return is used, pass it to the callback
      next(undefined, result);
    }
    lock = false;
  }

  // 用于并行的生成器 Add a special callback generator `this.parallel()` that groups stuff.
  next.parallel = function () {
    var index = 1 + counter++;
    pending++; // 开启了一个新的异步任务

    return function () {
      pending--;// 计算器减 1,表示执行完毕
      // 如果有错误,则保存在结果数组的第一个元素中 Compress the error from any result to the first argument
      if (arguments[0]) {
        results[0] = arguments[0];
      }
      // 按次序保存结果 Send the other results as arguments
      results[index] = arguments[1];
      if (!lock && pending === 0) {// 最后才执行 
        // 当所有分支都搞定,执行最后一个回调。When all parallel branches done, call the callback
        next.apply(null, results);
      }
    };
  };

  // Generates a callback generator for grouped results
  next.group = function () {
    var localCallback = next.parallel();
    var counter = 0;
    var pending = 0;
    var result = [];
    var error = undefined;

    function check() {
      if (pending === 0) {
        // When group is done, call the callback
        localCallback(error, result);
      }
    }
    process.nextTick(check); // Ensures that check is called at least once

    // Generates a callback for the group
    return function () {
      var index = counter++;
      pending++;
      return function () {
        pending--;
        // Compress the error from any result to the first argument
        if (arguments[0]) {
          error = arguments[0];
        }
        // Send the other results as arguments
        result[index] = arguments[1];
        if (!lock) { check(); }
      };
    };
  };

  // 开始工作流 Start the engine an pass nothing to the first step.
  next();
}

/**

相当于一个包装步骤列表的工厂
*/
// Tack on leading and tailing steps for input and output and return
// the whole thing as a function. Basically turns step calls into function
// factories.
Step.fn = function StepFn() {
  var steps = Array.prototype.slice.call(arguments);
  return function () {
    var args = Array.prototype.slice.call(arguments);

    // Insert a first step that primes the data stream
    var toRun = [function () {
      this.apply(null, args);
    }].concat(steps);

    // 加入最后的步骤 If the last arg is a function add it as a last step
    if (typeof args[args.length-1] === 'function') {
      toRun.push(args.pop());
    }


    Step.apply(null, toRun);
  }
}


// CommonJS模块系统的钩子 Hook into commonJS module systems
if (typeof module !== 'undefined' && "exports" in module) {
  module.exports = Step;
}

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

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

简单测试 代码

Step(function(a){
	throw 'err';
	return 1;
}, function(err, b){
	console.log(err);
	return [b, 2];
}, function(err, c){
	console.log(err);
	alert(c);
});

Step 缺点是使用 try...catch 包裹业务代码,使得异常全部被捕获,在调试阶段,这是不友好的。那么怎么破?可以将调试器设置为“Pause on all exception”。

目录
相关文章
|
3月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
142 19
|
4月前
|
JSON 前端开发 Serverless
Mock.js 语法结构全解析
Mock.js 的语法规范介绍,从数据模板定义规范和数据占位符定义规范俩部分介绍, 让你更好的使用 Mock.js 来模拟数据并提高开发效率。
|
6月前
|
资源调度 JavaScript 前端开发
前端开发必备!Node.js 18.x LTS保姆级安装教程(附国内镜像源配置)
本文详细介绍了Node.js的安装与配置流程,涵盖环境准备、版本选择(推荐LTS版v18.x)、安装步骤(路径设置、组件选择)、环境验证(命令测试、镜像加速)及常见问题解决方法。同时推荐开发工具链,如VS Code、Yarn等,并提供常用全局包安装指南,帮助开发者快速搭建高效稳定的JavaScript开发环境。内容基于官方正版软件,确保合规性与安全性。
5565 24
|
6月前
|
设计模式 XML 算法
策略模式(Strategy Pattern)深度解析教程
策略模式属于行为型设计模式,通过定义算法族并将其封装为独立的策略类,使得算法可以动态切换且与使用它的客户端解耦。该模式通过组合替代继承,符合开闭原则(对扩展开放,对修改关闭)。
|
6月前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
164 17
|
6月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
180 4
|
6月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6月前
|
存储 JavaScript 前端开发
全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略
如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
6月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS