async源码学习(一):waterfall

某天写完nodejs,忽然福至心灵想去看看async的源码长什么样子,打开一看原来就1000来行,非常简洁。于是一边看一边写下这篇文章,作为学习笔记记录下来。

关于waterfall

Runs the tasks array of functions in series, each passing their results to the next in the array. However, if any of the tasks pass an error to their own callback, the next function is not executed, and the maincallback is immediately called with the error.

按顺序依次执行一组函数。每个函数产生的结果依次传给数组中的下一个函数。如果有一个有任何一个函数的产生了错误,那么它的下一个函数就不会执行,而是执行最后的回调函数(maincallback,就是第二个参数)来处理这个错误。

waterfall的参数如下:

Name Type Description
tasks Array An array of functions to run, each function is passed a callback(err, result1, result2, ...) it must call on completion. The first argument is an error (which can be null) and any further arguments will be passed as arguments in order to the next task.
callback function <optional> An optional callback to run once all the functions have completed. This will be passed the results of the last task's callback. Invoked with (err, [results]).

waterfall源码

由于是直接看的手上项目里的源码,所以版本比较老,最新版貌似已经把每个方法都拆成一个个小模块了。先将就看着,等有机会在把最新的补上,比较下看看有什么不同:)

async.waterfall = function (tasks, callback) {
    // 一些边界情况的处理
    // 参数callback未定义时,默认执行一个空的函数
    callback = callback || function () {};
    // 传入参数tasks不是数组时报错
    if (tasks.constructor !== Array) {
      var err = new Error('First argument to waterfall must be an array of functions');
      return callback(err);
    }
    // 空数组直接调用callback并返回
    if (!tasks.length) {
        return callback();
    }
    // 定义了wrapIterator函数
    var wrapIterator = function (iterator) {
        // 此处省略若干行
    };
    // 执行wranpIterator返回的函数
    wrapIterator(async.iterator(tasks))();
};

这里大约可以分为3部分,第一段是对一些边界条件的处理,第二段定义了一个函数wrapIterator对iterator进行了一层包装,第三段执行包装后的wrapIterator。

为了能够更好的理解waterfall的工作流程,我们先来看看async.iterator()这个函数

async.iterator = function (tasks) {
    // makeCallback相当于对每个tasks里的函数进行一层包装
    // 包装后的函数多了一个next属性
    var makeCallback = function (index) {
        // 定义一个函数对象fn
        // 对象结构为{ [Function: fn] next: [Function] }
        var fn = function () {
            // fn执行时会执行task[index],并将fn的参数传给tasks[index]
            if (tasks.length) {
                tasks[index].apply(null, arguments);
            }
            // fn执行完以后会返回tasks中的下个函数
            return fn.next();
        };
        // next指向makeCallback包装后的task[index + 1]
        fn.next = function () {
            return (index < tasks.length - 1) ? makeCallback(index + 1): null;
        };
        // fn就是包装后的tasks[index]
        return fn;
    };
    // 返回第一个fn,于是我们得到了一个迭代器
    return makeCallback(0);
};

每调用一次makeCallback就会包装一个task为iterator,而在fn.next指向下个iterator后又会递归的调用makeCallback,因此makeCallback(0)相当于完成了两件事,递归的把每个task包装为iterator,并返回了第一个iterator的引用。

抽象点说,iterator函数接受一个函数数组,并把它串起来以后变成了一个迭代器

用起来大致就是这样的:

var tasks = [
    function () {
        console.log(1);
    },
    function () {
        console.log(2);
    }
];

var iterator = async.iterator(tasks);

iterator = iterator();
iterator = iterator();

输出:

1
2

现在我们可以看下wrapIterator函数了,源码如下

var wrapIterator = function (iterator) {
    // 如果是err,则跳转到最后的callback
    return function (err) {
        if (err) {
            callback.apply(null, arguments);
            callback = function () {};
        }
        else {
            // args是传到下个function的参数,第一个err是不需要的
            // 要注意的是arguments并不是一个array
            // 所以对arguments做切片要用如下方式
            var args = Array.prototype.slice.call(arguments, 1);
            // 下一个迭代器
            var next = iterator.next();
            // 把下一个包装后的wrapIterator作为最后个参数传入
            if (next) {
                args.push(wrapIterator(next));
            }
            else {
                args.push(callback);
            }
            // 执行iterator
            // 这个iterator会执行一遍task,并在回调处调用下一个wrapIterator
            async.setImmediate(function () {
                iterator.apply(null, args);
            });
        }
    };
};

这个wrapIterator声明了一个匿名函数,这个function实际上就是我们task中间用来处理err的那个“中转”了,类似一个调度器,对传入的第一个err进行辨别,如果有err则直接调最后的callback而不是下一个task。传入的iterator,就是我们要执行的下个tasks。

总结

waterfall的实现并不困难,有兴趣的话可以自己尝试下。源码实现的非常简洁,借用了iterator这个函数实现了一次包装,将一个数组变成了迭代器,再用一个调度器去执行迭代器。


补充最新版本代码

版本是v2.1.4,现在async做了模块化,每一个函数都封装到了一个独立的文件夹。源代码来源于官方文档:
官方文档

源代码

import isArray from 'lodash/isArray';
import noop from 'lodash/noop';
import once from './internal/once';
import rest from './internal/rest';
import onlyOnce from './internal/onlyOnce';
/**
 * Runs the `tasks` array of functions in series, each passing their results to
 * the next in the array. However, if any of the `tasks` pass an error to their
 * own callback, the next function is not executed, and the main `callback` is
 * immediately called with the error.
 *
 * @name waterfall
 * @static
 * @memberOf module:ControlFlow
 * @method
 * @category Control Flow
 * @param {Array} tasks - An array of functions to run, each function is passed
 * a `callback(err, result1, result2, ...)` it must call on completion. The
 * first argument is an error (which can be `null`) and any further arguments
 * will be passed as arguments in order to the next task.
 * @param {Function} [callback] - An optional callback to run once all the
 * functions have completed. This will be passed the results of the last task's
 * callback. Invoked with (err, [results]).
 * @returns undefined
 * @example
 *
 * async.waterfall([
 *     function(callback) {
 *         callback(null, 'one', 'two');
 *     },
 *     function(arg1, arg2, callback) {
 *         // arg1 now equals 'one' and arg2 now equals 'two'
 *         callback(null, 'three');
 *     },
 *     function(arg1, callback) {
 *         // arg1 now equals 'three'
 *         callback(null, 'done');
 *     }
 * ], function (err, result) {
 *     // result now equals 'done'
 * });
 *
 * // Or, with named functions:
 * async.waterfall([
 *     myFirstFunction,
 *     mySecondFunction,
 *     myLastFunction,
 * ], function (err, result) {
 *     // result now equals 'done'
 * });
 * function myFirstFunction(callback) {
 *     callback(null, 'one', 'two');
 * }
 * function mySecondFunction(arg1, arg2, callback) {
 *     // arg1 now equals 'one' and arg2 now equals 'two'
 *     callback(null, 'three');
 * }
 * function myLastFunction(arg1, callback) {
 *     // arg1 now equals 'three'
 *     callback(null, 'done');
 * }
 */
export default  function(tasks, callback) {
    callback = once(callback || noop);
    // 一些边界处理
    if (!isArray(tasks)) return callback(new Error('First argument to waterfall must be an array of functions'));
    if (!tasks.length) return callback();
    var taskIndex = 0;
    // 调用这个函数,则把args传给下一个task,并递归调用自身,直到最后个task
    // 这个函数就是task和task之间的中转,负责处理err和args的传递
    function nextTask(args) {
        // 如果执行完了最后一个task,则调用callback
        if (taskIndex === tasks.length) {
            return callback.apply(null, [null].concat(args));
        }
        // 给下一个task包装了一层处理
        var taskCallback = onlyOnce(rest(function(err, args) {
            if (err) {
                return callback.apply(null, [err].concat(args));
            }
            nextTask(args);
        }));
        // 把包装后的下一个task作为最后个参数压入当前task的参数栈
        args.push(taskCallback);
        // 取出准备调用的task,taskIndex自增
        var task = tasks[taskIndex++];
        // 调用task
        task.apply(null, args);
    }
    // 调用第一个task
    nextTask([]);
}

小结

比起原来的代码,新代码更简洁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容