A.2 asynquence API
首先,创建序列(一个 asynquence 实例)的方法是通过函数 ASQ(..) 。没有参数的 ASQ() 调用会创建一个空的初始序列,而向 ASQ(..) 函数传递一个或多个值,则会创建一个序列,其中每个参数表示序列中的一个初始步骤。
为了使这里所有的代码示例起见,我将在全局浏览器使用 asynquence 顶级标识符:ASQ 。如果你通过模块系统(浏览器或服务器)包含并使用 asynquence 的话,当然可以定义任何你喜欢的符号,asynquence 不会在意!
这里讨论的许多 API 方法是构建在 asynquence 的核心库中的,还有其他一些是通过包含可选的 contrib 插件包提供的。请参考 asynquence 文档(http://github.com/getify/asynquence ),确定一个方法是内建的还是通过插件定义的。
A.2.1 步骤
如果一个函数表示序列中的一个普通步骤,那调用这个函数时第一个参数是 continuation 回调,所有后续的参数都是从前一个步骤传递过来的消息。直到这个 continuation 回调被调用后,这个步骤才完成。一旦它被调用,传给它的所有参数将会作为消息传入序列中的下一个步骤。
要向序列中添加额外的普通步骤,可以调用 then(..) (这本质上和 ASQ(..) 调用的语义完全相同):
ASQ( // 步骤1 function(done){ setTimeout( function(){ done( "Hello" ); }, 100 ); }, // 步骤2 function(done,greeting) { setTimeout( function(){ done( greeting + " World" ); }, 100 ); } ) // 步骤3 .then( function(done,msg){ setTimeout( function(){ done( msg.toUpperCase() ); }, 100 ); } ) // 步骤4 .then( function(done,msg){ console.log( msg ); // HELLO WORLD } );
尽管 then(..) 和原生 Promise API 名称相同,但是这个 then(..) 是不一样的。你可以向 then(..) 传递任意多个函数或值,其中每一个都会作为一个独立步骤。其中并不涉及两个回调的完成 / 拒绝语义。
和 Promise 不同的一点是:在 Promise 中,如果你要把一个 Promise 链接到下一个,需要创建这个 Promise 并通过 then(..) 完成回调函数返回这个 Promise;而使用 asynquence,你需要做的就是调用 continuation 回调——我一直称之为 done() ,但你可以随便给它取什么名字——并可选择性将完成消息传递给它作为参数。
通过 then(..) 定义的每个步骤都被假定为异步的。如果你有一个同步的步骤,那你可以直接调用 done(..) ,也可以使用更简单的步骤辅助函数 val(..) 。
// 步骤1(同步) ASQ( function(done){ done( "Hello" ); // 手工同步 } ) // 步骤2(同步) .val( function(greeting){ return greeting + " World"; } ) // 步骤3(异步) .then( function(done,msg){ setTimeout( function(){ done( msg.toUpperCase() ); }, 100 ); } ) // 步骤4(同步) .val( function(msg){ console.log( msg ); } );
可以看到,通过 val(..) 调用的步骤并不接受 continuation 回调,因为这一部分已经为你假定了,结果就是参数列表没那么凌乱!如果要给下一个步骤发送消息的话,只需要使用 return 。
可以把 val(..) 看作一个表示同步的“只有值”的步骤,可以用于同步值运算、日志记录及其他类似的操作。
A.2.2 错误
与 Promise 相比,asynquence 一个重要的不同之处就是错误处理。
通过 Promise,链中每个独立的 Promise(步骤)都可以有自己独立的错误,接下来的每个步骤都能处理(或者不处理)这个错误。这个语义的主要原因(再次)来自于对单独 Promise 的关注而不是将链(序列)作为整体。
我相信,多数时候,序列中某个部分的错误通常是不可恢复的,所以序列中后续的步骤也就没有意义了,应该跳过。因此,在默认情况下,一个序列中任何一个步骤出错都会把整个序列抛入出错模式中,剩余的普通步骤会被忽略。
如果你确实需要一个错误可恢复的步骤,有几种不同的 API 方法可以实现,比如 try(..) (前面作为一种 try..catch 步骤提到过)或者 until(..) (一个重试循环,会尝试步骤直到成功或者你手工使用 break() )。asynquence 甚至还有 pThen(..) 和 pCatch(..) 方法,它们和普通的 Promise then(..) 和 catch(..) 的工作方式完全一样(参见第 3 章)。因此,如果你愿意的话,可以定制序列当中的错误处理。
关键在于,你有两种选择,但根据我的经验,更常用的是默认的那个。通过 Promise,为了使一个步骤链在出错时忽略所有步骤,你需要小心地避免在任意步骤中注册拒绝处理函数。否则的话,这个错误就会因被当作已经处理的而被吞掉,同时这个序列可能会继续(很可能是出乎意料的)。要正确可靠地处理这一类需求有点棘手。
asynquence 为注册一个序列错误通知处理函数提供了一个 or(..) 序列方法。这个方法还有一个别名,onerror(..) 。你可以在序列的任何地方调用这个方法,也可以注册任意多个处理函数。这很容易实现多个不同的消费者在同一个序列上侦听,以得知它有没有失败。从这个角度来说,它有点类似错误事件处理函数。
和使用 Promise 类似,所有的 JavaScript 异常都成为了序列错误,或者你也可以编写代码来发送一个序列错误信号:
var sq = ASQ( function(done){ setTimeout( function(){ // 为序列发送出错信号 done.fail( "Oops" ); }, 100 ); } ) .then( function(done){ // 不会到达这里 } ) .or( function(err){ console.log( err ); // Oops } ) .then( function(done){ // 也不会到达这里 } ); // 之后 sq.or( function(err){ console.log( err ); // Oops } );
asynquence 的错误处理和原生 Promise 还有一个非常重要的区别,就是默认状态下未处理异常的行为。正如在第 3 章中讨论过的,没有注册拒绝处理函数的被拒绝 Promise 就会默默地持有(即吞掉)这个错误。你需要记得总要在链的尾端添加一个最后的 catch(..) 。
而在 asynquence 中,这个假定是相反的。
如果一个序列中发生了错误,并且此时没有注册错误处理函数,那这个错误就会被报告到控制台。换句话说,未处理的拒绝在默认情况下总是会被报告,而不会被吞掉和错过。
一旦你针对某个序列注册了错误处理函数,这个序列就不会产生这样的报告,从而避免了重复的噪音。
实际上,可能在一些情况下你会想创建一个序列,这个序列可能会在你能够注册处理函数之前就进入了出错状态。这不常见,但偶尔也会发生。
在这样的情况下,你可以选择通过对这个序列调用 defer() 来避免这个序列实例的错误报告。应该只有在确保你最终会处理这种错误的情况下才选择关闭错误报告:
var sq1 = ASQ( function(done){ doesnt.Exist(); // 将会向终端抛出异常 } ); var sq2 = ASQ( function(done){ doesnt.Exist(); // 只抛出一个序列错误 } ) // 显式避免错误报告 .defer(); setTimeout( function(){ sq1.or( function(err){ console.log( err ); // ReferenceError } ); sq2.or( function(err){ console.log( err ); // ReferenceError } ); }, 100 ); // ReferenceError (from sq1)
这种错误处理方式要好于 Promise 本身的那种行为,因为它是成功的坑,而不是失败陷阱(参见第 3 章)。
如果向一个序列插入(包括了)另外一个序列,参见 A.2.5 节中的完整描述,那么源序列就会关闭错误报告,但是必须要考虑现在目标序列的错误报告开关的问题。
A.2.3 并行步骤
并非序列中的所有步骤都恰好执行一个(异步)任务。序列中的一个步骤中如果有多个子步骤并行执行则称为 gate(..) (还有一个别名 all(..) ,如果你愿意用的话),和原生的 Promise.all([..]) 直接对应。
如果 gate(..) 中所有的步骤都成功完成,那么所有的成功消息都会传给下一个序列步骤。如果它们中有任何一个出错的话,整个序列就会立即进入出错状态。
考虑:
ASQ( function(done){ setTimeout( done, 100 ); } ) .gate( function(done){ setTimeout( function(){ done( "Hello" ); }, 100 ); }, function(done){ setTimeout( function(){ done( "World", "!" ); }, 100 ); } ) .val( function(msg1,msg2){ console.log( msg1 ); // Hello console.log( msg2 ); // [ "World", "!" ] } );
出于展示说明的目的,我们把这个例子与原生 Promise 对比:
new Promise( function(resolve,reject){ setTimeout( resolve, 100 ); } ) .then( function(){ return Promise.all( [ new Promise( function(resolve,reject){ setTimeout( function(){ resolve( "Hello" ); }, 100 ); } ), new Promise( function(resolve,reject){ setTimeout( function(){ // 注:这里需要一个[ ]数组 resolve( [ "World", "!" ] ); }, 100 ); } ) ] ); } ) .then( function(msgs){ console.log( msgs[0] ); // Hello console.log( msgs[1] ); // [ "World", "!" ] } );
Promise 用来表达同样的异步流程控制的重复样板代码的开销要多得多。这是一个很好的展示,说明了为什么 asynquence 的 API 和抽象让 Promise 步骤的处理轻松了很多。异步流程越复杂,改进就会越明显。
1. 步骤的变体
contrib 插件中提供了几个 asynquence 的 gate(..) 步骤类型的变体,非常实用。
· any(..) 类似于 gate(..) ,除了只需要一个子步骤最终成功就可以使得整个序列前进。
· first(..) 类似于 any(..) ,除了只要有任何步骤成功,主序列就会前进(忽略来自其他步骤的后续结果)。
· race(..) (对应 Promise.race([..]) )类似于 first(..) ,除了只要任何步骤完成(成功或失败),主序列就会前进。
· last(..) 类似于 any(..) ,除了只有最后一个成功完成的步骤会将其消息发送给主序列。
· none(..) 是 gate(..) 相反:只有所有的子步骤失败(所有的步骤出错消息被当作成功消息发送,反过来也是如此),主序列才前进。
让我们先定义一些辅助函数,以便更清楚地进行说明:
function success1(done) { setTimeout( function(){ done( 1 ); }, 100 ); } function success2(done) { setTimeout( function(){ done( 2 ); }, 100 ); } function failure3(done) { setTimeout( function(){ done.fail( 3 ); }, 100 ); } function output(msg) { console.log( msg ); }
现在来说明这些 gate(..) 步骤变体的用法:
ASQ().race( failure3, success1 ) .or( output ); // 3 ASQ().any( success1, failure3, success2 ) .val( function(){ var args = [].slice.call( arguments ); console.log( args // [ 1, undefined, 2 ] ); } ); ASQ().first( failure3, success1, success2 ) .val( output ); // 1 ASQ().last( failure3, success1, success2 ) .val( output ); // 2 ASQ().none( failure3 ) .val( output ) // 3 .none( failure3 success1 ) .or( output ); // 1
另外一个步骤变体是 map(..) ,它使你能够异步地把一个数组的元素映射到不同的值,然后直到所有映射过程都完成,这个步骤才能继续。map(..) 与 gate(..) 非常相似,除了它是从一个数组而不是从独立的特定函数中取得初始值,而且这也是因为你定义了一个回调函数来处理每个值:
function double(x,done) { setTimeout( function(){ done( x * 2 ); }, 100 ); } ASQ().map( [1,2,3], double ) .val( output ); // [2,4,6]
map(..) 的参数(数组或回调)都可以从前一个步骤传入的消息中接收:
function plusOne(x,done) { setTimeout( function(){ done( x + 1 ); }, 100 ); } ASQ( [1,2,3] ) .map( double ) // 消息[1,2,3]传入 .map( plusOne ) // 消息[2,4,6]传入 .val( output ); // [3,5,7]
另外一个变体是 waterfall(..) ,这有点类似于 gate(..) 的消息收集特性和 then(..) 的顺序处理特性的混合。
首先执行步骤 1,然后步骤 1 的成功消息发送给步骤 2,然后两个成功消息发送给步骤 3,然后三个成功消息都到达步骤 4,以此类推。这样,在某种程度上,这些消息集结和层叠下来就构成了“瀑布”(waterfall)。
考虑:
function double(done) { var args = [].slice.call( arguments, 1 ); console.log( args ); setTimeout( function(){ done( args[args.length - 1] * 2 ); }, 100 ); } ASQ( 3 ) .waterfall( double, // [ 3 ] double, // [ 6 ] double, // [ 6, 12 ] double // [ 6, 12, 24 ] ) .val( function(){ var args = [].slice.call( arguments ); console.log( args ); // [ 6, 12, 24, 48 ] } );
如果“瀑布”中的任何一点出错,整个序列就会立即进入出错状态。
2. 容错
有时候可能需要在步骤级别上管理错误,不让它们把整个序列带入出错状态。为了这个目的,asynquence 提供了两个步骤变体。
try(..) 会试验执行一个步骤,如果成功的话,这个序列就和通常一样继续。如果这个步骤失败的话,失败就会被转化为一个成功消息,格式化为 { catch: .. } 的形式,用出错消息填充:
ASQ() .try( success1 ) .val( output ) // 1 .try( failure3 ) .val( output ) // { catch: 3 } .or( function(err){ // 永远不会到达这里 } );
也可以使用 until(..) 建立一个重试循环,它会试着执行这个步骤,如果失败的话就会在下一个事件循环 tick 重试这个步骤,以此类推。
这个重试循环可以无限继续,但如果想要从循环中退出的话,可以在完成触发函数中调用标志 break() ,触发函数会使主序列进入出错状态:
var count = 0; ASQ( 3 ) .until( double ) .val( output ) // 6 .until( function(done){ count++; setTimeout( function(){ if (count < 5) { done.fail(); } else { // 跳出until(..)重试循环 done.break( "Oops" ); } }, 100 ); } ) .or( output ); // Oops
3. Promise 风格的步骤
如果你喜欢在序列使用类似于 Promise 的 then(..) 和 catch(..) (参见第 3 章)的 Promise 风格语义,可以使用 pThen 和 pCatch 插件:
ASQ( 21 ) .pThen( function(msg){ return msg * 2; } ) .pThen( output ) // 42 .pThen( function(){ // 抛出异常 doesnt.Exist(); } ) .pCatch( function(err){ // 捕获异常(拒绝) console.log( err ); // ReferenceError } ) .val( function(){ // 主序列以成功状态返回, // 因为之前的异常被 pCatch(..)捕获了 } );
pThen(..) 和 pCatch(..) 是设计用来运行在序列中的,但其行为方式就像是在一个普通的 Promise 链中。因此,可以从传给 pThen(..) 的完成处理函数决议真正的 Promise 或 asynquence 序列(参见第 3 章)。
A.2.4 序列分叉
关于 Promise,有一个可能会非常有用的特性,那就是可以附加多个 then(..) 处理函数注册到同一个 promise;在这个 promise 处有效地实现了分叉流程控制:
var p = Promise.resolve( 21 ); // 分叉1(来自p) p.then( function(msg){ return msg * 2; } ) .then( function(msg){ console.log( msg ); // 42 } ) // 分叉2 (来自p) p.then( function(msg){ console.log( msg ); // 21 } );
在 asynquence 里可使用 fork() 实现同样的分叉:
var sq = ASQ(..).then(..).then(..); var sq2 = sq.fork(); // 分叉1 sq.then(..)..; // 分叉2 sq2.then(..)..;
A.2.5 合并序列
如果要实现 fork() 的逆操作,可以使用实例方法 seq(..) ,通过把一个序列归入另一个序列来合并这两个序列:
var sq = ASQ( function(done){ setTimeout( function(){ done( "Hello World" ); }, 200 ); } ); ASQ( function(done){ setTimeout( done, 100 ); } ) // 将sq序列纳入这个序列 .seq( sq ) .val( function(msg){ console.log( msg ); // Hello World } )
正如这里展示的,seq(..) 可以接受一个序列本身,或者一个函数。如果它接收一个函数,那么就要求这个函数被调用时会返回一个序列。因此,前面的代码可以这样实现:
// .. .seq( function(){ return sq; } ) // ..
这个步骤也可以通过 pipe(..) 来完成:
// .. .then( function(done){ // 把sq加入done continuation回调 sq.pipe( done ); } ) // ..
如果一个序列被包含,那么它的成功消息流和出错流都会输入进来。
正如前面的注解所提到的,管道化(使用 pipe(..) 手工实现的或通过 seq(..) 自动进行的)会关闭源序列的错误报告,但不会影响目标序列的错误报告。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论