B.1 可迭代序列
附录 A 介绍过 asynquence 的可迭代序列,这里我们打算更深入地再次探讨一下相关内容。
回忆一下:
var domready = ASQ.iterable(); // .. domready.val( function(){ // DOM就绪 } ); // .. document.addEventListener( "DOMContentLoaded", domready.next );
现在,让我们把一个多步骤序列定义为可迭代序列:
var steps = ASQ.iterable(); steps .then( function STEP1(x){ return x * 2; } ) .steps( function STEP2(x){ return x + 3; } ) .steps( function STEP3(x){ return x * 4; } ); steps.next( 8 ).value; // 16 steps.next( 16 ).value; // 19 steps.next( 19 ).value; // 76 steps.next().done; // true
可以看到,可迭代序列是一个符合标准的迭代器(参见第 4 章)。因此,可通过 ES6 的 for..of 循环迭代,就像生成器(或其他任何 iterable)一样:
var steps = ASQ.iterable(); steps .then( function STEP1(){ return 2; } ) .then( function STEP2(){ return 4; } ) .then( function STEP3(){ return 6; } ) .then( function STEP4(){ return 8; } ) .then( function STEP5(){ return 10; } ); for (var v of steps) { console.log( v ); } // 2 4 6 8 10
除了附录 A 中的事件触发示例之外,可迭代序列的有趣之处在于它们从本质上可以看作是一个生成器或 Promise 链的替身,但其灵活性却更高。
请考虑一个多 Ajax 请求的例子。我们在第 3 章和第 4 章中已经看到过同样的场景,分别通过 Promise 链和生成器实现的。用可迭代序列来表达:
// 支持序列的ajax var request = ASQ.wrap( ajax ); ASQ( "http://some.url.1" ) .runner( ASQ.iterable() .then( function STEP1(token){ var url = token.messages[0]; return request( url ); } ) .then( function STEP2(resp){ return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .then( function STEP3(r1,r2){ return r1 + r2; } ) ) .val( function(msg){ console.log( msg ); } );
可迭代序列表达了一系列顺序的(同步或异步的)步骤,看起来和 Promise 链非常相似。换句话说,它比直接的回调嵌套看起来要简洁得多,但没有生成器的基于 yield 的顺序语法那么好。
但我们把可迭代序列传入了 ASQ#runner(..) ,这个函数会把该序列执行完毕,就像对待生成器那样。可迭代序列本质上和生成器的行为方式一样。这个事实值得注意,原因如下。
首先,可迭代序列是 ES6 生成器某个子集的某种前 ES6 等价物。也就是说,你可以直接编写它们(在任意环境运行),或者你也可以编写 ES6 生成器,并将其重编译或转化为可迭代序列(就此而言,也可以是 Promise 链!)。
把“异步完整运行”的生成器看作是 Promise 链的语法糖,对于认识它们的同构关系是很重要的。
在继续之前,我们应该注意到,前面的代码片段可以用 asynquence 重写如下:
ASQ( "http://some.url.1" ) .seq( /*STEP 1*/ request ) .seq( function STEP2(resp){ return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .val( function STEP3(r1,r2){ return r1 + r2; } ) .val( function(msg){ console.log( msg ); } );
而且,步骤 2 也可以这样写:
.gate( function STEP2a(done,resp) { request( "http://some.url.2/?v=" + resp ) .pipe( done ); }, function STEP2b(done,resp) { request( "http://some.url.3/?v=" + resp ) .pipe( done ); } )
那么,如果更简单平凡的 asynquence 链就可以做得很好的话,为什么还要辛苦地把我们的流程控制表达为 ASQ#runner(..) 步骤中的可迭代序列呢?
因为可迭代序列形式还有很重要的秘密,提供了更强大的功能。请继续阅读。
可迭代序列扩展
生成器、普通 asynquence 序列以及 Promise 链都是及早求值 (eagerly evaluated)——不管最初的流程控制是什么,都会执行这个固定的流程。
然而,可迭代序列是惰性求值 (lazily evaluated),这意味着在可迭代序列的执行过程中,如果需要的话可以用更多的步骤扩展这个序列。
只能在可迭代序列的末尾添加步骤,不能插入序列的中间。
首先,让我们通过一个简单点的(同步)例子来熟悉一下这个功能:
function double(x) { x *= 2; // 应该继续扩展吗? if (x < 500) { isq.then( double ); } return x; } // 建立单步迭代序列 var isq = ASQ.iterable().then( double ); for (var v = 10, ret; (ret = isq.next( v )) && !ret.done; ) { v = ret.value; console.log( v ); }
一开始这个可迭代序列只定义了一个步骤(isq.then(double) ),但这个可迭代序列在某种条件下(x < 500 )会持续扩展自己。严格说来,asynquence 序列和 Promise 链也可以实现类似的功能,不过我们很快将说明为什么它们的能力是不足的。
尽管这个例子很平常,也可以通过一个生成器中的 while 循环表达,但我们会考虑到更复杂的情况。
举例来说,可以查看 Ajax 请求的响应,如果它指出还需要更多的数据,就有条件地向可迭代序列中插入更多的步骤来发出更多的请求。或者你也可以有条件地在 Ajax 处理结尾处增加一个值格式化的步骤。
考虑:
var steps = ASQ.iterable() .then( function STEP1(token){ var url = token.messages[0].url; // 提供了额外的格式化步骤了吗? if (token.messages[0].format) { steps.then( token.messages[0].format ); } return request( url ); } ) .then( function STEP2(resp){ // 向区列中添加一个Ajax请求吗? if (/x1/.test( resp )) { steps.then( function STEP5(text){ return request( "http://some.url.4/?v=" + text ); } ); } return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .then( function STEP3(r1,r2){ return r1 + r2; } );
你可以看到,在两个不同的位置处,我们有条件地使用 steps.then(..) 扩展了 steps 。要运行这个可迭代序列 steps ,只需要通过 ASQ#runner(..) 把它链入我们的带有 asynquence 序列(这里称为 main )的主程序流程:
var main = ASQ( { url: "http://some.url.1", format: function STEP4(text){ return text.toUpperCase(); } } ) .runner( steps ) .val( function(msg){ console.log( msg ); } );
可迭代序列 steps 的这一灵活性(有条件行为)可以用生成器表达吗?算是可以吧,但我们不得不以一种有点笨拙的方式重新安排这个逻辑:
function *steps(token) { // 步骤1 var resp = yield request( token.messages[0].url ); // 步骤2 var rvals = yield ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); // 步骤3 var text = rvals[0] + rvals[1]; // 步骤4 //提供了额外的格式化步骤了吗? if (token.messages[0].format) { text = yield token.messages[0].format( text ); } // 步骤5 // 需要向序列中再添加一个Ajax请求吗? if (/foobar/.test( resp )) { text = yield request( "http://some.url.4/?v=" + text ); } return text; } // 注意:*steps()可以和前面的steps一样被同一个ASQ序列运行
除了已经确认的生成器的顺序、看似同步的语法的好处(参见第 4 章),要模拟可扩展可迭代序列 steps 的动态特性,steps 的逻辑也需要以 *steps() 生成器形式重新安排。
而如果要通过 Promise 或序列来实现这个功能会怎样呢?你可以这么做:
var steps = something( .. ) .then( .. ) .then( function(..){ // .. // 扩展链是吧? steps = steps.then( .. ); // .. }) .then( .. );
其中的问题捕捉起来比较微妙,但是很重要。所以,考虑要把我们的 steps Promise 链链入主程序流程。这次使用 Promise 来表达,而不是 asynquence:
var main = Promise.resolve( { url: "http://some.url.1", format: function STEP4(text){ return text.toUpperCase(); } } ) .then( function(..){ return steps; // hint! } ) .val( function(msg){ console.log( msg ); } );
现在能看出问题所在了吗?仔细观察!
序列步骤排序有一个竞态条件。在你返回 steps 的时候,steps 这时可能是之前定义的 Promise 链,也可能是现在通过 steps = steps.then(..) 调用指向扩展后的 Promise 链。根据执行顺序的不同,结果可能不同。
以下是两个可能的结果。
· 如果 steps 仍然是原来的 Promise 链,一旦之后它通过 steps = steps.then(..) 被“扩展”,在链结尾处扩展之后的 promise 就不会被 main 流程考虑,因为它已经连到了 steps 链。很遗憾,这就是及早求值 的局限性。
· 如果 steps 已经是扩展后的 Promise 链,它就会按预期工作,因为 main 连接的是扩展后的 promise。
除了竞态条件这个无法接受的事实,第一种情况也需要担心,它展示了 Promise 链的及早求值 。与之对比的是,我们很容易扩展可迭代序列,且不会有这样的问题,因为可迭代序列是惰性求值 的。
你所需的流程控制的动态性越强,可迭代序列的优势就越明显。
在 asynquence 网站上可以得到关于可迭代序列的更多信息和示例(https://github.com/getify/asynquence/blob/master/README.md#iterable-sequences )。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论