【你不懂JS: 异步与性能】附录A: *asynquence* 库

第一章和第二章相当详细地探讨了常见的异步编程模式,以及如何通过回调解决它们。但我们也看到了为什么回调在处理能力上有着致命的缺陷,这将我们带到了第三章和第四章,Promise 与 Generator 为你的异步流程构建提供了一个更加坚实,可信,以及可推理的基础。

我在这本书中好几次提到我自己的异步库 asynquence (http://github.com/getify/asynquence) —— “async” + “sequence” = “asynquence”,现在我想简要讲解一下它的工作原理,以及它的独特设计为什么很重要和很有用。

在下一篇附录中,我们将要探索一些高级的异步模式,但为了它们的可用性能够使人接受你可能需要一个库。我们将使用 asynquence 来表达这些模式,所以你会想首先在这里花一点时间来了解这个库。

asynquence 绝对不是优秀异步编码的唯一选择;在这方面当然有许多了不起的库。但是 asynquence 提供了一种独特的视角 —— 通过将这些模式中最好的部分组合进一个单独的库,另外它基于一个基本的抽象:(异步)序列。

我的前提是,精巧的JS程序经常或多或少地需要将各种不同的异步模式交织在一起,而且这通常是完全依靠每个开发者自己去搞清楚的。与其引入关注于异步流程的不同方面的两个或更多的库,asynquence 将它们统一为各种序列步骤,成为单独一个需要学习和部署的核心库。

我相信 asynquence 有足够高的价值可以使 Promise 风格的异步流程控制编程变得超级容易完成,这就是我们为什么会在这里单单关注这个库。

开始之前,我将讲解 asynquence 背后的设计原则,然后我们将使用代码示例来展示它的API如何工作。

序列,抽象设计

asynquence 的理解开始于对一个基础抽象的理解:对于一个任务的任何一系列步骤来说,无论它们是同步的还是异步的,都可以被综合地考虑为一个“序列(sequence)”。换句话说,一个序列是一个容器,它代表一个任务,并由一个个完成这个任务的独立的(可能是异步的)步骤组成。

在这个序列中的每一个步骤都处于一个 Promise(见第三章) 的控制之下。也就是你向一个序列添加的每一个步骤都隐含地创建了一个 Promise,它被链接到这个序列的末尾。由于 Promise 的语义,在一个序列中的每一个步骤的推进都是异步的,即使你同步地完成这个步骤。

另外,一个序列将总是一步一步线性地进行,也就是步骤2总是发生在步骤1完成之后,如此类推。

当然,一个新的序列可以从既存的序列中分支出来,也就是分支仅在主序列在流程中到达那一点时发生。序列还可以用各种方式组合,包括使一个序列在流程中的一个特定的位置汇合另一个序列。

一个序列与 Promise 链有些相像。但是,在 Promise 链中,不存在一个可以引用整个链条的“把手”可以抓住。不管你持有哪一个 Promise 的引用,它都表示链条中当前的步骤外加挂载在它后面的其他步骤。实质上,你无法持有一个 Promise 链条的引用,除非你持有链条中第一个 Promise 的引用。

许多情况表明,持有一个综合地指向整个序列的引用是十分有用的。这些情况中最重要的一种就是序列的退出/取消。正如我们在第三章中展开谈过的那样,Promise 本身绝不应当是可以取消的,因为这违反了一个基本设计规则:外部不可变性。

但是序列没有这样的不可变性设计原则,这主要是由于序列不会作为需要不可变语义的未来值的容器被传递。所以序列是一个处理退出/取消行为的恰当的抽象层面。asynquence 序列可以在任何时候abort(),而且这个序列将会停止在那一点而不会因为任何原因继续下去。

为了流程控制,还有许多理由首选序列的抽象而非 Promise 链。

首先,Promise 链是一个更加手动的处理 —— 一旦你开始在你的程序中大面积地创建和链接 Promise ,这种处理可能会变得相当烦冗 —— 在那些使用 Promise 相当恰当的地方,这种烦冗会降低效率而使得开发者不愿使用Promise。

抽象意味着减少模板代码和烦冗,所以序列抽象是这个问题的一个好的解决方案。使用 Promise,你关注的是个别的步骤,而且不太会假定你将延续这个链条。而序列采用相反的方式,它假定序列将会无限地持续添加更多步骤。

当你开始考虑更高阶的 Promise 模式时(除了race([..])all([..])以外),这种抽象复杂性的降低特别强大。

例如,在一个序列的中间,你可能想表达一个在概念上类似于try..catch的步骤,它的结果将总是成功,不管是意料之中的主线上的成功解析,还是为被捕获的错误提供一个正面的非错误信号。或者,你可能想表达一个类似于 retry/until 循环的步骤,它不停地尝试相同的步骤直到成功为止。

仅仅使用基本的 Promise,这类抽象不是很容易表达,而且在一个既存的 Promise 链的中间这样做不好看。但如果你将你的想法抽象为一个序列,并将一个步骤考虑为一个 Promise 的包装,这个包装可以隐藏这样的细节,它就可以使你以最合理的方式考虑流程控制,而不必关心细节。

第二,也许是更重要的,将异步流程控制考虑为一个序列中的步骤,允许你将这样的细节抽象出去 —— 每一个步骤中引入了哪一种异步性。在这种抽象之下,一个 Promise 将总是控制着步骤,但在抽象之上,这个步骤可以看起来像一个延续回调(简单的默认值),或者一个真正的 Promise,或者一个运行至完成的 Generator,或者… 希望你明白我的意思。

第三,序列可以通容易地被调整来适应于不同的思考模式,比如基于事件的,基于流的,或者基于相应式的编码。asynquence 提供了一种我称为“响应式序列”的模式(我们稍后讲解),它是 RxJS(“Reactive Extensions”) 中“响应式可监听”思想的变种,允许重复的事件每次触发一个新的序列实例。Promise 是一次性的,所以单独使用 Promise 来表达重复的异步性十分尴尬。

在一种我称为“可迭代序列”的模式中,另一种思考模式反转了解析/控制能力。与每一个步骤在内部控制它自己的完成(并因此推进这个序列)不同,序列被反转为通过一个外部迭代器来进行推进控制,而且在这个 可迭代序列 中的每一步仅仅应答next(..)迭代器 控制。

在本附录的剩余部分,我们将探索所有这些不同的种类,所以如果我们刚才的步伐太快也不要担心。

要点是,对于复杂的异步处理来说,序列是一个要比单纯的 Promise(Promise链)或单纯的 Generator 更加强大与合理的抽象,而 asynquence 被设计为使用恰当层面的语法糖来表达这种抽象,使得异步编程变得更加易于理解和更加令人愉快。

asynquence API

首先,你创建一个序列(一个 asynquence 实例)的方法是使用ASQ(..)函数。一个不带参数的ASQ()调用会创建一个空的初始序列,而向ASQ(..)传递一个或多个值或函数的话,它会使用每个参数值代表序列的初始步骤来创建序列。

注意: 为了这里所有的代码示例,我将使用 asynquence 在浏览器全局作用域中的顶层标识符:ASQ。如果你通过一个模块系统(在浏览器或服务器中)引入并使用 asynquence,你当然可以定义自己喜欢的符号,asynquence 不会关心这些!

许多在这里讨论的API方法都内建于 asynquence 的核心部分,而其他的API是通过引入可选的“contrib”插件包提供的。要知道一个方法是内建的还是通过插件定义的,可以参见 asynquence 的文档:http://github.com/getify/asynquence

步骤

如果一个函数代表序列中的一个普通步骤,那么这个函数会被这样调用:第一个参数是延续回调,而任何后续参数都是从前一个步骤中传递下来的消息。在延续回调被调用之前,这个步骤将不会完成。一旦延续回调被调用,你传递给它的任何参数值都会作为序列下一个步骤中的消息被发送。

要向一个序列添加额外的普通步骤,调用then(..)(它实质上与ASQ(..)调用的语义完全相同):

  1. ASQ(
  2. // 步骤 1
  3. function(done){
  4. setTimeout( function(){
  5. done( "Hello" );
  6. }, 100 );
  7. },
  8. // 步骤 2
  9. function(done,greeting) {
  10. setTimeout( function(){
  11. done( greeting + " World" );
  12. }, 100 );
  13. }
  14. )
  15. // 步骤 3
  16. .then( function(done,msg){
  17. setTimeout( function(){
  18. done( msg.toUpperCase() );
  19. }, 100 );
  20. } )
  21. // 步骤 4
  22. .then( function(done,msg){
  23. console.log( msg ); // HELLO WORLD
  24. } );

注意: 虽然then(..)这个名称与原生的 Promise API 完全一样,但是这个then(..)的含义是不同的。你可以传递任意多或者任意少的函数或值给then(..),而它们中的每一个都被看作是一个分离的步骤。这里与完成/拒绝语义的双回调毫不相干。

在 Promise 中,可以把一个 Promise 与下一个你在then(..)的完成处理器中创建并return的 Promise 链接。与此不同的是,在 asynquence 中,你所需要做的一切就是调用延续回调 —— 我总是称之为done(),但你可以起任何适合你的名字 —— 并将完成的消息作为参数值选择性地传递给它。

通过then(..)定义的每一个步骤都被认为是异步的。如果你有一个同步的步骤,你可以立即调用done(..),或者使用更简单的val(..)步骤帮助函数:

  1. // 步骤 1(同步)
  2. ASQ( function(done){
  3. done( "Hello" ); // 手动同步
  4. } )
  5. // 步骤 2(同步)
  6. .val( function(greeting){
  7. return greeting + " World";
  8. } )
  9. // 步骤 3(异步)
  10. .then( function(done,msg){
  11. setTimeout( function(){
  12. done( msg.toUpperCase() );
  13. }, 100 );
  14. } )
  15. // 步骤 4(同步)
  16. .val( function(msg){
  17. console.log( msg );
  18. } );

如你所见,val(..)调用的步骤不会收到一个延续回调,因为这部分已经为你做好了 —— 而且参数列表作为一个结果显得不那么凌乱了!要向下一个步骤发送消息,你简单地使用return

val(..)考虑为表示一个同步的“仅含有值”的步骤,它对同步的值操作,比如 logging 之类,非常有用。

错误

与 Promise 相比 asynquence 的一个重要的不同之处是错误处理。

在 Promise 链条中,每个 Promise(步骤)都可以拥有自己独立的错误,而每个后续的步骤都有能力处理或不处理这个错误。这种语义(再一次)主要来自于对每个单独的 Promise 的关注,而非对整个链条(序列)的关注。

我相信,在大多数情况下,一个位于序列中某一部分的错误通常是不可恢复的,所以序列中后续的步骤毫无意义而应当被跳过。所以,默认情况下,在一个序列的任意一个步骤中的错误会将整个序列置于错误模式,而剩下的普通步骤将会被忽略。

如果你 确实 需要一个错误可以被恢复的步骤,有几个不同的API可以适应这种情况,比如try(..) —— 先前提到过的,有些像try..catch的步骤 —— 或者until(..) —— 一个重试循环,它持续地尝试一个步骤直到它成功或你手动地break()这个循环。asynquence 甚至拥有pThen(..)pCatch(..)方法,它们的工作方式与普通的 Promise 的then(..)catch(..)(见第三章)完全相同,所以如果你选择这么做,你就可以进行本地化的序列中错误处理。

重点是,你同时拥有两个选项,但是在我的经验中更常见的是默认情况。使用 Promise,要使一个步骤的链条在错误发生时一次性忽略所有步骤,你不得不小心不要在任何步骤中注册拒绝处理器;否则,这个错误会被视为处理过而被吞掉,而序列可能仍会继续下去(也许不是意料之中的)。要恰当且可靠地处理这种期待的行为有点儿尴尬。

要注册一个序列错误通知处理器,asynquence 提供了一个or(..)序列方法,它还有一个别名叫做onerror(..)。你可以在序列的任何位置调用这个方法,而且你可以注册任意多的处理器。这使得让多个不同的消费者监听一个序列是否失败变得很容易;从这个角度讲,它有点儿像一个错误事件处理器。

正如使用 Promise 那样,所有JS异常都会变为序列错误,或者你可以通过编程来发生一个序列错误:

  1. var sq = ASQ( function(done){
  2. setTimeout( function(){
  3. // 为序列发出一个错误
  4. done.fail( "Oops" );
  5. }, 100 );
  6. } )
  7. .then( function(done){
  8. // 永远不会到达这里
  9. } )
  10. .or( function(err){
  11. console.log( err ); // Oops
  12. } )
  13. .then( function(done){
  14. // 也不会到达这里
  15. } );
  16. // 稍后
  17. sq.or( function(err){
  18. console.log( err ); // Oops
  19. } );

asynquence 与原生的 Promise 相比,在错误处理上另一个重要的不同就是“未处理异常”的默认行为。正如我们在第三章中以相当的篇幅讨论过的,一个没有被注册拒绝处理器的 Promise 如果被拒绝的话,将会无声地保持(也就是吞掉)那个错误;你不得不总是想着要用一个最后的catch(..)来终结一个链条。

asynquence 中,这种假设被颠倒过来了。

如果一个错误在序列上发生,而且 在那个时刻 它没有被注册错误处理器,那么这个错误会被报告至console。换言之,未处理的的拒绝将总是默认地被报告,因此不会被吞掉或丢掉。

为了防止重复的噪音,只要你向一个序列注册一个错误处理器,它就会使这个序列从这样的报告中退出。

事实上有许多情况你想要创建这样一个序列,它可能会在你有机会注册处理器之前就进入错误状态。这不常见,但可能时不时地发生。

在这样的情况下,你也可以通过在序列上调用defer()来使一个序列实例 从错误报告中退出。你应当仅在自己确信不会最终处理这样的错误时,才决定从报告中退出:

  1. var sq1 = ASQ( function(done){
  2. doesnt.Exist(); // 将会向控制台抛出异常
  3. } );
  4. var sq2 = ASQ( function(done){
  5. doesnt.Exist(); // 仅仅会抛出一个序列错误
  6. } )
  7. // 错误报告中的退出
  8. .defer();
  9. setTimeout( function(){
  10. sq1.or( function(err){
  11. console.log( err ); // ReferenceError
  12. } );
  13. sq2.or( function(err){
  14. console.log( err ); // ReferenceError
  15. } );
  16. }, 100 );
  17. // ReferenceError (来自sq1)

这是一种比 Promise 本身拥有的更好的错误处理行为,因为它是一个成功的深渊,而不是一个失败的深渊(参见第三章)。

注意: 如果一个序列被导入(也就是被汇合入)另一个序列 —— 完整的描述参见“组合序列” —— 之后源序列从错误报告中退出,那么就必须考虑目标序列是否进行错误报告。

并行步骤

在你的序列中不是所有的步骤都将只拥有一个(异步)任务去执行;有些将会需要“并行”(并发地)执行多个步骤。在一个序列中,一个并发地处理多个子步骤的步骤称为一个gate(..) —— 如果你喜欢的话它还有一个别名all(..) —— 而且它与原生的Promise.all([..])是对称的。

如果在gate(..)中的所有步骤都成功地完成了,那么所有成功的消息都将被传递到下一个序列步骤中。如果它们中的任何一个产生了一个错误,那么整个序列会立即进入错误状态。

考虑如下代码:

  1. ASQ( function(done){
  2. setTimeout( done, 100 );
  3. } )
  4. .gate(
  5. function(done){
  6. setTimeout( function(){
  7. done( "Hello" );
  8. }, 100 );
  9. },
  10. function(done){
  11. setTimeout( function(){
  12. done( "World", "!" );
  13. }, 100 );
  14. }
  15. )
  16. .val( function(msg1,msg2){
  17. console.log( msg1 ); // Hello
  18. console.log( msg2 ); // [ "World", "!" ]
  19. } );

为了展示差异,让我们把这个例子与原生 Promise 比较一下:

  1. new Promise( function(resolve,reject){
  2. setTimeout( resolve, 100 );
  3. } )
  4. .then( function(){
  5. return Promise.all( [
  6. new Promise( function(resolve,reject){
  7. setTimeout( function(){
  8. resolve( "Hello" );
  9. }, 100 );
  10. } ),
  11. new Promise( function(resolve,reject){
  12. setTimeout( function(){
  13. // 注意:这里我们需要一个 [ ]
  14. resolve( [ "World", "!" ] );
  15. }, 100 );
  16. } )
  17. ] );
  18. } )
  19. .then( function(msgs){
  20. console.log( msgs[0] ); // Hello
  21. console.log( msgs[1] ); // [ "World", "!" ]
  22. } );

讨厌。Promise 需要多得多的模板代码来表达相同的异步流程控制。这个例子很好地说明了为什么 asynquence API 和抽象使得对付 Promise 步骤容易多了。你的异步流程越复杂,它的改进程度就越高。

各种步骤

关于 asynquencegate(..)步骤类型,有好几种不同的 contrib 插件可能十分有用:

  • any(..)很像gate(..),除了为了继续主序列,只需要有一个环节最终必须成功。
  • first(..)很像any(..),除了只要有任何一个环节成功,主序列就会继续(忽略任何其余环节产生的后续结果)。
  • race(..)(与Promise.race([..])对称)很像first(..),除了主序列会在任何环节完成时(不管成功还是失败)立即继续。
  • last(..)很像any(..),除了只有最后一个环节成功完成时才会把它的消息发送给主序列。
  • none(..)gate(..)的反义:主序列仅在所有环节失败时才会继续(将所有环节的错误消息作为成功消息传送,或者反之)。

让我们首先定义一些帮助函数来使示例清晰一些:

  1. function success1(done) {
  2. setTimeout( function(){
  3. done( 1 );
  4. }, 100 );
  5. }
  6. function success2(done) {
  7. setTimeout( function(){
  8. done( 2 );
  9. }, 100 );
  10. }
  11. function failure3(done) {
  12. setTimeout( function(){
  13. done.fail( 3 );
  14. }, 100 );
  15. }
  16. function output(msg) {
  17. console.log( msg );
  18. }

现在,让我们展示一些这些gate(..)步骤的变种:

  1. ASQ().race(
  2. failure3,
  3. success1
  4. )
  5. .or( output ); // 3
  6. ASQ().any(
  7. success1,
  8. failure3,
  9. success2
  10. )
  11. .val( function(){
  12. var args = [].slice.call( arguments );
  13. console.log(
  14. args // [ 1, undefined, 2 ]
  15. );
  16. } );
  17. ASQ().first(
  18. failure3,
  19. success1,
  20. success2
  21. )
  22. .val( output ); // 1
  23. ASQ().last(
  24. failure3,
  25. success1,
  26. success2
  27. )
  28. .val( output ); // 2
  29. ASQ().none(
  30. failure3
  31. )
  32. .val( output ) // 3
  33. .none(
  34. failure3
  35. success1
  36. )
  37. .or( output ); // 1

另一个步骤种类是map(..),它让你将一个数组的元素异步地映射为不同的值,而且在所有映射完成之前步骤不会前进。map(..)gate(..)十分相似,除了它从一个数组,而非从一个指定的分离函数那里得到初始值,而且你定义一个函数回调来操作每一个值:

  1. function double(x,done) {
  2. setTimeout( function(){
  3. done( x * 2 );
  4. }, 100 );
  5. }
  6. ASQ().map( [1,2,3], double )
  7. .val( output ); // [2,4,6]

另外,map(..)可以从前一步骤传递来的消息中收到它的两个参数(数组或者回调):

  1. function plusOne(x,done) {
  2. setTimeout( function(){
  3. done( x + 1 );
  4. }, 100 );
  5. }
  6. ASQ( [1,2,3] )
  7. .map( double ) // 收到消息`[1,2,3]`
  8. .map( plusOne ) // 收到消息`[2,4,6]`
  9. .val( output ); // [3,5,7]

另一个种类是waterfall(..),它有些像混合了gate(..)的消息收集行为与then(..)的序列化处理。

步骤1首先被执行,然后来自步骤1的成功消息被传递给步骤2,然后两个成功消息走到步骤3,然后所有三个成功消息走到步骤4,如此继续,这样消息被某种程度上收集并从“瀑布”上倾泻而下。

考虑如下代码:

  1. function double(done) {
  2. var args = [].slice.call( arguments, 1 );
  3. console.log( args );
  4. setTimeout( function(){
  5. done( args[args.length - 1] * 2 );
  6. }, 100 );
  7. }
  8. ASQ( 3 )
  9. .waterfall(
  10. double, // [ 3 ]
  11. double, // [ 6 ]
  12. double, // [ 6, 12 ]
  13. double // [ 6, 12, 24 ]
  14. )
  15. .val( function(){
  16. var args = [].slice.call( arguments );
  17. console.log( args ); // [ 6, 12, 24, 48 ]
  18. } );

如果在“瀑布”的任何一点发生错误,那么整个序列就会立即进入错误状态。

容错

有时你想在步骤一级管理错误,而不一定让它们使整个序列成为错误状态。asynquence 为此提供了两种步骤类型。

try(..)尝试一个步骤,如果它成功,序列就会正常继续,但如果这个步骤失败了,失败的状态会转换成格式为{ catch: .. }的成功消息,它的值由错误消息填充:

  1. ASQ()
  2. .try( success1 )
  3. .val( output ) // 1
  4. .try( failure3 )
  5. .val( output ) // { catch: 3 }
  6. .or( function(err){
  7. // 永远不会到达这里
  8. } );

你还可以使用until(..)构建一个重试循环,它尝试一个步骤,如果失败,就会在下一个事件轮询的 tick 中重试这个步骤,如此继续。

这种重试循环可以无限延续下去,但如果你想要从循环中跳出来,你可以在完成触发器上调用break()标志方法,它将主序列置为错误状态:

  1. var count = 0;
  2. ASQ( 3 )
  3. .until( double )
  4. .val( output ) // 6
  5. .until( function(done){
  6. count++;
  7. setTimeout( function(){
  8. if (count < 5) {
  9. done.fail();
  10. }
  11. else {
  12. // 跳出 `until(..)` 重试循环
  13. done.break( "Oops" );
  14. }
  15. }, 100 );
  16. } )
  17. .or( output ); // Oops

Promise 式的步骤

如果你喜欢在你的序列中内联 Promise 风格的语义,比如 Promise 的then(..)catch(..)(见第三章),你可以使用pThenpCatch插件:

  1. ASQ( 21 )
  2. .pThen( function(msg){
  3. return msg * 2;
  4. } )
  5. .pThen( output ) // 42
  6. .pThen( function(){
  7. // 抛出一个异常
  8. doesnt.Exist();
  9. } )
  10. .pCatch( function(err){
  11. // 捕获这个异常(拒绝)
  12. console.log( err ); // ReferenceError
  13. } )
  14. .val( function(){
  15. // 主旋律回归到正常状态,
  16. // 因为前一个异常已经被
  17. // `pCatch(..)`捕获了
  18. } );

pThen(..)pCatch(..)被设计为运行在序列中,但好像在普通的 Promise 链中动作。这样,你就可以在传递给pThen(..)的“完成”处理器中解析纯粹的 Promise 或者 asynquence 序列。

序列分支

一个有关 Promise 的可能十分有用的特性是,你可以在同一个 Promise 上添附多个then(..)处理器,这实质上在这个 Promise 的流程上创建了“分支”:

  1. var p = Promise.resolve( 21 );
  2. // (从`p`开始的)分支 1
  3. p.then( function(msg){
  4. return msg * 2;
  5. } )
  6. .then( function(msg){
  7. console.log( msg ); // 42
  8. } )
  9. // (从`p`开始的)分支 2
  10. p.then( function(msg){
  11. console.log( msg ); // 21
  12. } );

使用 asynquencefork()可以很容易地进行同样的“分支”:

  1. var sq = ASQ(..).then(..).then(..);
  2. var sq2 = sq.fork();
  3. // 分支 1
  4. sq.then(..)..;
  5. // 分支 2
  6. sq2.then(..)..;

组合序列

fork()相反的是,你可以通过将一个序列汇合进另一个来组合两个序列,使用seq(..)实例方法:

  1. var sq = ASQ( function(done){
  2. setTimeout( function(){
  3. done( "Hello World" );
  4. }, 200 );
  5. } );
  6. ASQ( function(done){
  7. setTimeout( done, 100 );
  8. } )
  9. // 将序列 `sq` 汇合进这个系列
  10. .seq( sq )
  11. .val( function(msg){
  12. console.log( msg ); // Hello World
  13. } )

seq(..)可以像这里展示的那样接收一个序列本身,或者接收一个函数。如果是一个函数,那么它会期待这个函数被调用时返回一个序列,所以前面的代码可以这样写:

  1. // ..
  2. .seq( function(){
  3. return sq;
  4. } )
  5. // ..

另外,这个步骤还可以使用pipe(..)来完成:

  1. // ..
  2. .then( function(done){
  3. // 将 `sq` 导入延续回调 `done`
  4. sq.pipe( done );
  5. } )
  6. // ..

当一个序列被汇合时,它的成功消息流和错误消息流都会被导入。

注意: 正如早先的注意事项中提到过的,导入会使源序列从错误报告中退出,但不会影响目标序列的错误报告状态。

值与错误序列

如果一个序列的任意一个步骤只是一个普通值,那么这个值就会被映射到这个步骤的完成消息中:

  1. var sq = ASQ( 42 );
  2. sq.val( function(msg){
  3. console.log( msg ); // 42
  4. } );

如果你想制造一个自动出错的序列:

  1. var sq = ASQ.failed( "Oops" );
  2. ASQ()
  3. .seq( sq )
  4. .val( function(msg){
  5. // 不会到达这里
  6. } )
  7. .or( function(err){
  8. console.log( err ); // Oops
  9. } );

你还可能想要自动地创建一个延迟的值或者延迟的错误序列。使用afterfailAfter contrib 插件,这很容易:

  1. var sq1 = ASQ.after( 100, "Hello", "World" );
  2. var sq2 = ASQ.failAfter( 100, "Oops" );
  3. sq1.val( function(msg1,msg2){
  4. console.log( msg1, msg2 ); // Hello World
  5. } );
  6. sq2.or( function(err){
  7. console.log( err ); // Oops
  8. } );

你还可以使用after'(..)在一个序列的中间插入一个延迟:

  1. ASQ( 42 )
  2. // 在这个序列中插入一个延迟
  3. .after( 100 )
  4. .val( function(msg){
  5. console.log( msg ); // 42
  6. } );

Promises 与回调

我认为 asynquence 序列在原生的 Promise 之上提供了许多价值,而且你会发现在很大程度上它在抽象层面上使用起来更舒适更强大。然而,将 asynquence 与其他非 asynquence 代码进行整合将是不可避免的现实。

使用promise(..)实例方法,你可以很容易地将一个 Promise(也就是 thenable —— 见第三章)汇合进一个序列:

  1. var p = Promise.resolve( 42 );
  2. ASQ()
  3. .promise( p ) // 本可以写做:`function(){ return p; }`
  4. .val( function(msg){
  5. console.log( msg ); // 42
  6. } );

要向相反的方向走,从一个序列的特定步骤中分支/出让一个 Promise,使用toPromise contrib 插件:

  1. var sq = ASQ.after( 100, "Hello World" );
  2. sq.toPromise()
  3. // 现在这是一个标准的 promise 链了
  4. .then( function(msg){
  5. return msg.toUpperCase();
  6. } )
  7. .then( function(msg){
  8. console.log( msg ); // HELLO WORLD
  9. } );

有好几种帮助设施可以在使用回调的系统中适配 asynquence。要从你的序列中自动地生成一个“错误优先风格”回调,来接入一个面向回调的工具,使用errfcb

  1. var sq = ASQ( function(done){
  2. // 注意:这里期待“错误优先风格”的回调
  3. someAsyncFuncWithCB( 1, 2, done.errfcb )
  4. } )
  5. .val( function(msg){
  6. // ..
  7. } )
  8. .or( function(err){
  9. // ..
  10. } );
  11. // 注意:这里期待“错误优先风格”的回调
  12. anotherAsyncFuncWithCB( 1, 2, sq.errfcb() );

你还可能想要创建一个工具的序列包装版本 —— 与第三章的“promisory”和第四章的“thunkory”相比较 —— asynquence 为此提供了ASQ.wrap(..)

  1. var coolUtility = ASQ.wrap( someAsyncFuncWithCB );
  2. coolUtility( 1, 2 )
  3. .val( function(msg){
  4. // ..
  5. } )
  6. .or( function(err){
  7. // ..
  8. } );

注意: 为了清晰(和有趣!),让我们为来自ASQ.wrap(..)的产生序列的函数杜撰另一个名词,就像这里的coolUtility。我提议“sequory”(“sequence” + “factory”)。

可迭代序列

一个序列普通的范例是,每一个步骤都负责完成它自己,进而推进这个序列。Promise 就是这样工作的。

不幸的是,有时你需要从外部控制一个 Promise/步骤,而这会导致尴尬的“能力抽取”。

考虑这个 Promise 的例子:

  1. var domready = new Promise( function(resolve,reject){
  2. // 不想把这个放在这里,因为在逻辑上
  3. // 它属于代码的另一部分
  4. document.addEventListener( "DOMContentLoaded", resolve );
  5. } );
  6. // ..
  7. domready.then( function(){
  8. // DOM 准备好了!
  9. } );

关于 Promise 的“能力抽取”范模式看起来像这样:

  1. var ready;
  2. var domready = new Promise( function(resolve,reject){
  3. // 抽取 `resolve()` 能力
  4. ready = resolve;
  5. } );
  6. // ..
  7. domready.then( function(){
  8. // DOM 准备好了!
  9. } );
  10. // ..
  11. document.addEventListener( "DOMContentLoaded", ready );

注意: 在我看来,这种反模式是一种尴尬的代码风格,但有些开发者喜欢,我不能理解其中的原因。

asynquence 提供一种我称为“可迭代序列”的反转序列类型,它将控制能力外部化(它在domready这样的情况下十分有用):

  1. // 注意:这里`domready`是一个控制序列的 *迭代器*
  2. var domready = ASQ.iterable();
  3. // ..
  4. domready.val( function(){
  5. // DOM 准备好了!
  6. } );
  7. // ..
  8. document.addEventListener( "DOMContentLoaded", domready.next );

与我们在这个场景中看到的东西比起来,可迭代序列还有很多内容。我们将在附录B中回过头来讨论它们。

运行 Generator

在第四章中,我们衍生了一种称为run(..)的工具,它可以将 generator 运行至完成,监听被yield的 Promise 并使用它们来异步推进 generator。asynquence 正好有一个这样的内建工具,称为runner(..)

为了展示,让我们首先建立一些帮助函数:

  1. function doublePr(x) {
  2. return new Promise( function(resolve,reject){
  3. setTimeout( function(){
  4. resolve( x * 2 );
  5. }, 100 );
  6. } );
  7. }
  8. function doubleSeq(x) {
  9. return ASQ( function(done){
  10. setTimeout( function(){
  11. done( x * 2)
  12. }, 100 );
  13. } );
  14. }

现在,我们可以在一个序列的中间使用runner(..)作为一个步骤:

  1. ASQ( 10, 11 )
  2. .runner( function*(token){
  3. var x = token.messages[0] + token.messages[1];
  4. // yield 一个真正的 promise
  5. x = yield doublePr( x );
  6. // yield 一个序列
  7. x = yield doubleSeq( x );
  8. return x;
  9. } )
  10. .val( function(msg){
  11. console.log( msg ); // 84
  12. } );

包装过的 Generator

你还可以创建自包装的 generator —— 也就是一个普通函数,运行你指定的 generator 并为它的完成返回一个序列 —— 通过ASQ.wrap(..)包装它:

  1. var foo = ASQ.wrap( function*(token){
  2. var x = token.messages[0] + token.messages[1];
  3. // yield 一个真正的 promise
  4. x = yield doublePr( x );
  5. // yield 一个序列
  6. x = yield doubleSeq( x );
  7. return x;
  8. }, { gen: true } );
  9. // ..
  10. foo( 8, 9 )
  11. .val( function(msg){
  12. console.log( msg ); // 68
  13. } );

runner(..)还能做很多很牛的事情,我们会在附录B中回过头来讨论它。

复习

asynquence 是一个在 Promise 之上的简单抽象 —— 一个序列是一系列(异步)步骤,它的目标是使各种异步模式更加容易使用,而在功能上没有任何妥协。

asynquence 的核心API与它的 contrib 插件中,除了我们在这篇附录中看到的内容以外还有其他的好东西,我们把对这些剩余功能的探索作为练习留给读者。

现在你看到了 asynquence 的实质与精神。关键点是,一个序列由许多步骤组成,而这些步骤可以使许多不同种类的 Promise,或者它们可以是一个 generator 运行器,或者… 选择由你来决定,你有完全的自由为你的任务采用恰当的任何异步流程控制逻辑。

如果你能理解这些 asynquence 代码段,那么你现在就可以相当快地学会这个库;它实际上没有那么难学!

如果你依然对它如何(或为什么!)工作感到模糊,那么在进入下一篇附录之前,你将会想要多花一点时间去查看前面的例子,并亲自把玩一下 asynquence。附录B将会在几种更高级更强大的异步模式中使用 asynquence

写于 2018年05月25日1519

如非特别注明,文章皆为原创。

转载请注明出处: https://www.liayal.com/article/5b07efb44b90272bebb17a6a

记小栈小程序上线啦~搜索【记小栈】【点击扫码】体验

你不想说点啥么?
😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕😈👿👹👺💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾👐👐🏻👐🏼👐🏽👐🏾👐🏿🙌🙌🏻🙌🏼🙌🏽🙌🏾🙌🏿👏👏🏻👏🏼👏🏽👏🏾👏🏿🙏🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿🤝👍👍🏻👍🏼👍🏽👍🏾👍🏿👎👎🏻👎🏼👎🏽👎🏾👎🏿👊👊🏻👊🏼👊🏽👊🏾👊🏿✊🏻✊🏼✊🏽✊🏾✊🏿

评论

~ 评论还没有,沙发可以有 O(∩_∩)O~