本文摘自我的博客,迎接人人去走走。
又是两周没写博客了,圣诞夜来水一发~
本日轻微看了下async的源码,认为很简短精华精辟,统共也才1000多行代码,许多值得进修的处所。主要看的是waterfall
模块,由于源码中有许多差别接口公用的部份,因而看完waterfall这个接口的全部流程,差不多就cover了一半的async源码了。
造轮子
在没有太多运用履历的状况下,直接看源码,能够会碰到一些不明所以的细节,看了能够也只能吸取很少的一部份。最好的体式格局我认为莫过于本身先造一遍轮子,再看源码了。
接口需求
waterfall这个接口的定名照样很抽象的
我要定义一个waterfall函数,满足以下需求:
能够根据Array给定的递次逐一实行
一切函数实行终了后,挪用指定的回调函数
前一个函数的输出作为后一个函数的输入
半途某一个函数实行失利,直接挪用回调函数完毕
需求的代码形貌以下:
async.waterfall([
function(callback) {
callback(null, 'one', 'two');
},
function(arg1, arg2, callback) {
console.log(arg1);
console.log(arg2);
// arg1 now equals 'one' and arg2 now equals 'two'
callback(null, 'three');
},
function(arg1, callback) {
console.log(arg1);
// arg1 now equals 'three'
callback(null, 'done');
}
], function (err, result) {
// result now equals 'done'
console.log(result);
});
// 希冀输出:
// one
// two
// three
// done
编码
代码组织了好一会儿,又调试了好一会后(中心碰到了一个关于arguments的坑,背面会讲),终究成型了。输出是根据预期的,和async源码运转的效果雷同,剖析也都写在诠释中:
var async = {};
async.waterfall = function (tasks, cb){
// 指向下一个将要实行的函数
var _index = 0;
/**
* 挪用用户指定的函数
*/
function _run(index, args, cb){
var task = tasks[index];
args.push(cb);
task.apply(null, args);
};
/**
* 由于涉及到控制流的转移,从框架转移到用户,再从用户转移到框架。
* 须要定义一个通报控制流的使者,就是这个_cb函数
* 1.框架转移到用户:挪用用户函数的同时,把_cb作为参数
* 2.用户转移到框架:用户挪用这个_cb,表明已实行完该函数,把控制交给框架。抑或完毕,抑或实行下一个函数
*/
function _cb(){
// 假如毛病了,直接回调最外层的cb
// 假如是末了一个,也直接挪用最外层的cb
if (arguments[0] || _index === tasks.length) {
return cb && cb.apply(null, arguments);
}
/**
* 掏出回调参数,作为下一个函数的输入
* 由于回调的第一个参数是毛病码,所以要去掉第一个
*/
// var rest = arguments.slice(1); //arguments并没有slice要领,因而如许会报错
var rest = [].slice.call(arguments, 1);
_run(_index++, rest, _cb);
};
// 假如用户没有指定要串行实行的函数,则直接挪用回调
if (tasks.length === 0) return cb && cb();
_run(_index++, [], _cb);
};
坑
踩的这个坑是关于arguments
的(在ES6语法中实在不引荐运用arguments的体式格局,由于语法已支撑了rest param)。我一向认为一个函数的arguments属性是一个Array,由于常常能够看到经由历程arguments[0]的体式格局去猎取参数,也从来没有质疑过。先来看看下面这一个例子:
function a (){
console.log(typeof arguments);
console.log(arguments);
console.log(arguments[0]);
console.log(arguments['0']);
console.log(arguments.length);
console.log([].slice.call(arguments, 1));
};
a('one', 'two', 'three');
/**
* 输出(chrome):
* object
* ["one", "two", "three"]
* one
* one
* 3
* ["two", "three"]
*
* 输出(node.js)
* object
* { '0': 'one', '1': 'two', '2': 'three' }
* one
* one
* 3
* [ 'two', 'three' ]
*/
能够看出,arguments对象并非一个array对象。在chrome中虽然看上去打印出来的是Array,但它是能够睁开的,内里另有许多参数。而且下标取值的时刻不光能够用数字,也能够用字符串来取值。这也是为何我写的代码诠释中arguments.slice(1);
的体式格局会实行毛病(slice是Array才有的要领)。然则[].slice.call(arguments, 1);
却能实行,申明arguments照样有一点slice的特征的,有点不太懂。认为它同时继续了dict和array两种对象的部份特征。
本来的轮子
贴上本来的代码完成:
async.waterfall = function (tasks, callback) {
// 这类体式格局也是很智慧的一种体式格局,能够替代 callback && callback()的体式格局
// noop 是一个空函数,什么也不实行
callback = _once(callback || noop);
if (!_isArray(tasks)) {
var err = new Error('First argument to waterfall must be an array of functions');
return callback(err);
}
if (!tasks.length) {
return callback();
}
function wrapIterator(iterator) {
return _restParam(function (err, args) {
if (err) {
callback.apply(null, [err].concat(args));
}
else {
var next = iterator.next();
if (next) {
args.push(wrapIterator(next));
}
else {
args.push(callback);
}
ensureAsync(iterator).apply(null, args);
}
});
}
wrapIterator(async.iterator(tasks))();
};
抛开一些异常处置惩罚的状况,就整体逻辑流程上照样有些区分的,下面就逐一来剖析一下。
迭代器
我是本身经由历程_index
的局部变量来纪录当前实行的函数的(得益于闭包的特征,这个局部变量能够一向保留着)。源码完成了一种迭代器的体式格局去治理传入的函数数组,异常文雅,支撑next特征,观赏一下:
async.iterator = function (tasks) {
function makeCallback(index) {
function fn() {
if (tasks.length) {
tasks[index].apply(null, arguments);
}
return fn.next();
}
fn.next = function () {
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
};
return fn;
}
return makeCallback(0);
};
经由历程async.iterator
包装今后返回的是一个迭代器对象,他同时又是一个函数能够直接实行,包装了用户传入的tasks中的第一个函数。
调理器
有了迭代器,还须要一个调理器才根据预期的流程串行实行须要的函数,同时处置惩罚参数通报的历程(我本身写的代码,调理的事情是由_cb一同做的)。
这个调理器完成的异常棒,由于它返回的也是一个函数,因而和迭代器是属于统一个维度的(假如是挪用者和被挪用者的关联则不属于统一维度,他们的挪用条理关联是统一层的)。_restParam
函数能够临时不必管它,由于从它的完成中能够看到,它本身和它参数中的函数是统一个维度的,它只是担任转换了一下参数的构造。完整能够明白为wrapIterator
返回的就是被_restParam包着的谁人函数,_restParam
只是一个参数构造的转换器,处置惩罚了参数构造不一致的题目。
function _restParam(func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex;
return function() {
var length = Math.max(arguments.length - startIndex, 0);
var rest = Array(length);
for (var index = 0; index < length; index++) {
rest[index] = arguments[index + startIndex];
}
switch (startIndex) {
case 0: return func.call(this, rest);
case 1: return func.call(this, arguments[0], rest);
}
// Currently unused but handle cases outside of the switch statement:
// var args = Array(startIndex + 1);
// for (index = 0; index < startIndex; index++) {
// args[index] = arguments[index];
// }
// args[startIndex] = rest;
// return func.apply(this, args);
};
}
回到调理器的高低文,在参数通报的历程当中,args是上一个函数的返回效果构成的数组,再把下一个迭代器包装一下作为该数组的末了一个元素。如许在挪用当前迭代器对应的函数的时刻,用户态高低文中的callback就是下一个用户态函数对应的迭代器了。全部控制流程完整处在用户层,框架层所做的事仅仅是参数构造的转换(毕竟apply函数须要的参数构造是数组,而函数挪用的时刻则是睁开的情势)。
奇淫技能
在浏览代码的历程当中看到了不少奇妙的用法
导出
在async源码末了有如许一段代码:
// Node.js
if (typeof module === 'object' && module.exports) {
module.exports = async;
}
// AMD / RequireJS
else if (typeof define === 'function' && define.amd) {
define([], function () {
return async;
});
}
// included directly via <script> tag
else {
root.async = async;
}
由于如今js运用的局限异常广,又有后端nodejs,又有前端js,须要顺应差别的导入操纵。这段代码就能够异常完美地做到关于差别导入体式格局的支撑,包含:
nodejs中的require语法
RequireJs的导入体式格局
html中script标签的导入体式格局
+null
在_restParam
代码中有以下一行:
startIndex = startIndex == null ? func.length - 1 : +startIndex;
刚开始我还不明白在一个变量前加一个+
有什么用,厥后本身尝试今后发明,关于一个null实行+操纵符它会变成0。这个特征异常奇妙地运用了一行代码同时处置惩罚了参数考证和转换的事情。
关于这个用法,我一个同砚也是前端js大牛JerryZhou在segmentfault上有一个回复,关于这个题目诠释得不错,有兴致的朋侪能够去参考一下。
异步回调
源码中的ensureAsync
要领技能性很强。当tasks中的函数一个个实行的历程当中,我们当然是愿望回调行动发作在当前的函数实行终了后。毕竟在当前的高低文中实行callback的时候点并不一定在当前函数的末了(它以后另有语句须要实行,有能够还会做一些收尾的事情,或许别的情势的逻辑)。由于callback实在就是tasks中的下一个函数,一旦实行callback,运转逻辑就跑到下一个使命了,而此时当前的使命还没有完成。这显然是不合理的。因而ensureAsync
在这里就处置惩罚了这部份逻辑。
function ensureAsync(fn) {
return _restParam(function (args) {
var callback = args.pop();
args.push(function () {
var innerArgs = arguments;
if (sync) {
async.setImmediate(function () {
callback.apply(null, innerArgs);
});
} else {
callback.apply(null, innerArgs);
}
});
var sync = true;
fn.apply(this, args);
sync = false;
});
}
在async库中,_restParam
照样很通用的,会在许多处所看到。代码里首先把本来的callback掏出来,做一层封装,再push到参数列表中。设立一个sync的标签,只有当fn函数实行终了后才会转变它的状况。如许一来,在本来的回调函数中会搜检这个标签,假如是转变过的,则直接实行回调,也就是下一个task(此时能够确保当前task已实行终了)。假如sync标签未转变,申明当前task并未实行终了,这个回调函数将在下一个事宜轮回的tick中被挪用,下面是setImmediate
要领。
async.setImmediate = _setImmediate ? _delay : async.nextTick;
var _delay = _setImmediate ? function(fn) {
// not a direct alias for IE10 compatibility
_setImmediate(fn);
} : function(fn) {
setTimeout(fn, 0);
};
总结
边看代码边写的博客,写完发明真的又多懂了好些东西,由于有些知识点是我在写的时刻才倏忽明白的。刚开始看源码的时刻能够只明白了百分之30,就认为值得分享,源码很不错。但当我写完的时刻,我认为已控制了七八成了。看来照样要多写东西啊,愿望我写的东西也能够协助宽大朋侪一同进修讨论。