与ES6生成器异步
与ES6生成器异步
ES6发电机:完整系列
既然您已经看到了ES6生成器,并且对它们更加满意 ,现在是时候真正使用它们来改善我们的实际代码了。
生成器的主要优点在于,它们提供了单线程的,具有同步外观的代码样式, 同时允许您将异步性隐藏为实现细节 。 这使我们可以非常自然地表达程序的步骤/语句的流程,而不必同时浏览异步语法和陷阱。
换句话说,通过将值(我们的生成器逻辑)的消耗与异步实现这些值的实现细节next(..)
生成器的迭代器的next(..)
分开 ,我们可以很好地实现功能/关注点的分离。
结果? 异步代码的所有功能,以及同步(外观)代码的所有易读性和可维护性。
那么我们如何完成这项壮举呢?
最简单的异步
最简单地说,生成器不需要任何额外的功能来处理程序尚不具备的异步功能。
例如,假设您已经有以下代码:
function makeAjaxCall(url,cb) {
// do some ajax fun
// call `cb(result)` when complete
}
makeAjaxCall( "http://some.url.1", function(result1){
var data = JSON.parse( result1 );
makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
});
} );
要使用生成器(不带任何其他修饰)来表示相同的程序,请按以下步骤操作:
function request(url) {
// this is where we're hiding the asynchronicity,
// away from the main code of our generator
// `it.next(..)` is the generator's iterator-resume
// call
makeAjaxCall( url, function(response){
it.next( response );
} );
// Note: nothing returned here!
}
function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
var it = main();
it.next(); // get it all started
让我们研究一下它是如何工作的。
request(..)
帮助程序基本上包装了正常的makeAjaxCall(..)
实用程序,以确保其回调调用生成器迭代器的next(..)
方法。
通过request("..")
调用,您会发现它没有返回值 (换句话说,它是undefined
)。 没什么大不了的,但是与本文后面的处理方式形成对比是很重要的:我们在这里有效地yield undefined
。
因此,我们称之为yield ..
(具有undefined
值),它实际上什么也没做,只是在那一点上暂停了生成器。 它要等到it.next(..)
调用恢复后,我们才排队(作为回调)在Ajax调用完成之后发生。
但是yield ..
表达的结果会怎样? 我们将其分配给变量result1
。 第一次调用Ajax的结果如何?
因为当it.next(..)
称为Ajax回调时,它会将Ajax响应传递给它,这意味着该值将在当前暂停的点(即中间)被发送回生成器。 result1 = yield ..
语句!
那真的很酷而且超级强大。 从本质上讲, result1 = yield request(..)
要求提供值 ,但是(几乎!)对我们来说是完全隐藏的-至少我们在这里不需要担心它-幕后的实现导致了这一步是异步的。 它通过隐藏yield
的暂停功能,然后将生成器的恢复功能分离到另一个函数中,从而实现了异步性,因此我们的主要代码只是发出一个同步(看上去)值请求 。
第二个result2 = yield result(..)
语句完全相同:它透明地暂停和恢复,并为我们提供了所需的值,而这一切都不会影响我们那时编码中异步性的任何细节。
当然, yield
存在,所以有一种微妙的暗示神奇的东西(也称为异步), 可在该点发生 。 但是,与嵌套回调的噩梦相比,甚至与承诺链的API开销相比, yield
都是一个很小的语法信号/开销。
还请注意,我说“可能会发生”。 就其本身而言,这是一件非常强大的事情。 上面的程序总是进行异步Ajax调用,但是如果没有,该怎么办? 如果以后我们将程序更改为具有先前(或预取)的Ajax响应的内存缓存,该怎么办? 或在我们的应用程序的URL路由器其他一些复杂性可能会在某些情况下,完成一个Ajax请求向右走 ,而不需要真正去从服务器上获取呢?
我们可以将request(..)
的实现更改为以下形式:
var cache = {};
function request(url) {
if (cache[url]) {
// "defer" cached response long enough for current
// execution thread to complete
setTimeout( function(){
it.next( cache[url] );
}, 0 );
}
else {
makeAjaxCall( url, function(resp){
cache[url] = resp;
it.next( resp );
} );
}
}
注意:这里的一个细微,棘手的细节是,在缓存已经有结果的情况下,需要setTimeout(..0)
延迟。 如果我们刚刚叫it.next(..)
马上,它会产生一个错误,因为(这是棘手的部分)发电机组没有技术上处于暂停状态呢 。 我们的函数调用request(..)
被完全第一评价,然后将yield
暂停。 所以,我们不能称之为it.next(..)
再次又立即里面request(..)
因为就在那个时候,发电机仍在运行( yield
尚未处理)。 但是,我们可以在当前执行线程完成后立即将其称为“ it.next(..)
”“稍后”,这是我们的setTimeout(..0)
“ hack”完成的。 下面我们将对此提供更好的答案。
现在,我们的主要生成器代码仍然如下所示:
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..
看到!? 我们的发电机逻辑(又名我们的流量控制 )没得变化都从上面的启用非缓存版本。
*main()
的代码仍然只要求一个值,并暂停直到它取回之前才继续。 在我们当前的情况下,该“暂停”可能相对较长(发出一个实际的服务器请求,可能要300-800ms),或者几乎是立即发生的( setTimeout(..0)
延迟hack)。 但是我们的流量控制不在乎。
这是将异步性抽象为实现细节的真正力量。
更好的异步
对于简单的异步生成器而言,上述方法非常好。 但是它将很快变得有限,因此我们需要一个更强大的异步机制来与我们的生成器配对,从而能够处理更多的繁重工作。 那个机制? 承诺 。
如果您仍然对ES6 Promises感到有些困惑,我写了一个由5部分组成的博文,涉及这些内容。 去看书吧 我等你回来。 <咯咯笑,咯咯笑>。 狡猾的老套异步笑话!
与我们最初的嵌套回调示例一样,此处较早的Ajax代码示例也遭受所有相同的“控制反转”问题(又称“回调地狱”)。 到目前为止,我们缺少一些观察结果:
- 没有明确的错误处理路径。 正如我们在上一篇文章中了解到的 ,我们可以检测到Ajax调用发生错误(以某种方式),并使用
it.throw(..)
将其传递回我们的生成器,然后在生成器逻辑中使用try..catch
来处理它。 但这只是连接到“后端”(处理生成器迭代器的代码)的更多手动工作,并且如果我们在程序中使用大量生成器,则可能不是可以重复使用的代码。 - 如果
makeAjaxCall(..)
实用程序不在我们的控制之下,并且碰巧多次调用该回调,或者同时发信号通知成功和错误,等等,那么我们的生成器将陷入困境(未捕获的错误,意外值等) 。 处理和预防此类问题需要大量重复的手动工作,也可能不便于携带。 - 通常,我们需要“并行”执行多个任务 (例如,同时执行两个Ajax调用)。 由于生成器
yield
语句每个都是单个暂停点,所以两个或多个不能同时运行-它们必须一次运行一次。 因此,不清楚如何在单个生成器yield
点上启动多个任务,而无需在幕后编写大量手动代码。
如您所见,所有这些问题都是可以解决的 ,但是谁真的想每次都重新发明这些解决方案。 我们需要一个更强大的模式,该模式专门设计为基于生成器的异步编码的可信赖,可重用的解决方案 。
那个图案? yield
承诺 ,让它们在履行时恢复生成器。
回想一下,我们确实yield request(..)
,而request(..)
实用程序没有任何返回值,因此实际上只是yield undefined
?
让我们稍微调整一下。 让我们将request(..)
实用程序更改为基于Promise的,以便它返回Promise,因此,我们yield
的实际上是Promise (而非undefined
)。
function request(url) {
// Note: returning a promise now!
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} );
}
现在request(..)
构造了一个Promise,当Ajax调用完成时将解决该Promise,我们返回该Promise,以便可以将其yield
。 接下来是什么?
我们将需要一个控制生成器迭代器的实用程序,该实用程序将接收那些yield
ed promises并将它们连接起来以恢复生成器(通过next(..)
)。 我现在将这个实用程序runGenerator(..)
:
// run (async) a generator to completion
// Note: simplified approach: no error handling here
function runGenerator(g) {
var it = g(), ret;
// asynchronously iterate over generator
(function iterate(val){
ret = it.next( val );
if (!ret.done) {
// poor man's "is it a promise?" test
if ("then" in ret.value) {
// wait on the promise
ret.value.then( iterate );
}
// immediate value: just send right back in
else {
// avoid synchronous recursion
setTimeout( function(){
iterate( ret.value );
}, 0 );
}
}
})();
}
注意的关键事项:
- 我们自动初始化发生器(创建其
it
的迭代器),和我们异步运行it
来完成(done:true
)。 - 我们寻找可以
yield
的承诺(也就是每次it.next(..)
调用的返回value
)。 如果是这样,我们通过在promise上注册then(..)
来等待它完成。 - 如果返回了任何立即值(又名非承诺值),我们只需将该值发送回生成器中,这样它就可以立即运行。
现在,我们如何使用它?
runGenerator( function *main(){
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );
am! 等等...那是与之前完全相同的生成器代码 ? 是的 再次,这是发电机的力量被炫耀。 事实上,我们现在创造的承诺, yield
荷兰国际集团出来,并恢复发电机在他们完成- 所有这一切都“隐藏”的实现细节! 它并没有真正隐藏,只是与消耗代码(我们生成器中的流控制)分开了。
通过等待yield
发出的promise,然后将其完成值发送回it.next(..)
, result1 = yield request(..)
会获得与之前完全相同的值。
但是,既然我们正在使用promise来管理生成器代码的异步部分,我们就可以通过仅回调的编码方法解决所有的反转/信任问题。 通过使用generators + promises,我们可以免费获得上述问题的所有这些解决方案:
- 现在,我们具有内置的错误处理功能,可以轻松进行连接。 我们上面没有在
runGenerator(..)
显示它,但是侦听一个it.throw(..)
错误并将其连接到它上并不困难it.throw(..)
-然后我们可以使用try..catch
在我们的生成器代码中捕获和处理错误。 - 我们得到了承诺所提供的所有控制/信任 。 不用担心,不必大惊小怪。
-
承诺之上有许多强大的抽象,可以自动处理多个“并行”任务等的复杂性。
例如,
yield Promise.all([ .. ])
将为“并行”任务获取一个承诺数组,并yield
一个承诺(供生成器处理),它等待所有子承诺完成(以任何顺序),然后再继续。 从yield
表达式中得到的回报(当诺言完成时)是所有子承诺响应的数组,按请求的顺序排列(因此,无论完成顺序如何,都是可预测的)。
首先,让我们探讨错误处理:
// assume: `makeAjaxCall(..)` now expects an "error-first style" callback (omitted for brevity)
// assume: `runGenerator(..)` now also handles error handling (omitted for brevity)
function request(url) {
return new Promise( function(resolve,reject){
// pass an error-first style callback
makeAjaxCall( url, function(err,text){
if (err) reject( err );
else resolve( text );
} );
} );
}
runGenerator( function *main(){
try {
var result1 = yield request( "http://some.url.1" );
}
catch (err) {
console.log( "Error: " + err );
return;
}
var data = JSON.parse( result1 );
try {
var result2 = yield request( "http://some.url.2?id=" + data.id );
} catch (err) {
console.log( "Error: " + err );
return;
}
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );
如果一个承诺拒收(或任何其他类型的错误/异常)的发生,而URL抓取正在发生的事情,承诺拒绝将被映射到一个生成错误(使用-未显示- it.throw(..)
在runGenerator(..)
),将被try..catch
语句捕获。
现在,让我们看一个更复杂的示例,它使用promise管理更多异步复杂性:
function request(url) {
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} )
// do some post-processing on the returned text
.then( function(text){
// did we just get a (redirect) URL back?
if (/^https?:\/\/.+/.test( text )) {
// make another sub-request to the new URL
return request( text );
}
// otherwise, assume text is what we expected to get back
else {
return text;
}
} );
}
runGenerator( function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] );
var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results );
console.log( "Search results: " + resp.value );
} );
Promise.all([ .. ])
构造了一个等待三个子承诺的runGenerator(..)
,正是那个主要的runGenerator(..)
yield
了,让runGenerator(..)
实用程序监听生成器的恢复。 子承诺可以接收到一个看起来像要重定向到另一个URL的响应,并将另一个子请求承诺链接到新位置。 要了解有关诺言链的更多信息,请阅读本文部分 。
许诺可以处理异步的任何类型的功能/复杂性,您都可以通过使用生成承诺(...的承诺)的生成器来yield
看似同步的代码好处。 这是两全其美。
runGenerator(..)
:库实用程序
我们必须在上面定义我们自己的runGenerator(..)
实用工具,以启用和消除此generator + promise runGenerator(..)
。 为了简洁起见,我们甚至省略了这种实用程序的完整实现,因为有更多与错误处理相关的细微差别需要处理。
但是,您不想编写自己的runGenerator(..)
吗?
我不这么认为。
各种各样的promise / async库提供了这样的实用程序。 我不会在这里介绍它们,但是您可以看一下Q.spawn(..)
, co(..)
lib等。
但是,我将简要介绍一下我自己的库的实用程序: asynquence的Runner runner(..)
插件 ,因为我认为它提供了一些优于其他库的独特功能。 如果您除了学习本文的简要介绍之外还想了解更多信息,我写了一个两部分的关于异步的深入博客文章系列 。
首先, 异步提供了实用程序,用于自动处理上述片段中的“错误优先样式”回调:
function request(url) {
return ASQ( function(done){
// pass an error-first style callback
makeAjaxCall( url, done.errfcb );
} );
}
好多了 ,不是!?
接着,asynquence的runner(..)
的插件消耗发电机右在asynquence序列的中间(异步一系列步骤),这样就可以在传递消息(多个)从前面的步骤,和你的发电机可以传递消息( s),进入下一步,所有错误都会按照您的期望自动传播:
// first call `getSomeValues()` which produces a sequence/promise,
// then chain off that sequence for more async steps
getSomeValues()
// now use a generator to process the retrieved values
.runner( function*(token){
// token.messages will be prefilled with any messages
// from the previous step
var value1 = token.messages[0];
var value2 = token.messages[1];
var value3 = token.messages[2];
// make all 3 Ajax requests in parallel, wait for
// all of them to finish (in whatever order)
// Note: `ASQ().all(..)` is like `Promise.all(..)`
var msgs = yield ASQ().all(
request( "http://some.url.1?v=" + value1 ),
request( "http://some.url.2?v=" + value2 ),
request( "http://some.url.3?v=" + value3 )
);
// send this message onto the next step
yield (msgs[0] + msgs[1] + msgs[2]);
} )
// now, send the final result of previous generator
// off to another request
.seq( function(msg){
return request( "http://some.url.4?msg=" + msg );
} )
// now we're finally all done!
.val( function(result){
console.log( result ); // success, all done!
} )
// or, we had some error!
.or( function(err) {
console.log( "Error: " + err );
} );
异步运行器 runner(..)
实用程序接收(可选)消息以启动生成器,该消息来自序列的上一步,并且可以在生成器中的token.messages
数组中进行访问。
然后,类似于我们上面使用runGenerator(..)
实用工具演示的runGenerator(..)
, runner(..)
侦听yield
ed promise或yield
asynquence序列(在本例中为ASQ().all(..)
序列) (“并行”步骤)),并在恢复生成器之前等待其完成。
生成器完成后,其yield
的最终值将输出到序列中的下一步。
而且,如果在此序列中的任何位置(甚至在生成器内部)发生任何错误,它将冒泡到注册的单个or(..)
错误处理程序中。
异步试图使promise和generator的混合和匹配尽可能地简单。 您可以*选择将任何生成器流与基于承诺的序列步流连接起来,如您所愿。
ES7 async
对于ES7时间线,有一个提议似乎很可能被接受,它可以创建另一种函数: async function
,就像一个自动生成器,该生成器自动包装在类似runGenerator(..)
的实用程序中(或异步) ' runner(..)
)。 这样,您可以发送promise,并且async function
自动将它们连接起来,以在完成时恢复自身(甚至不需要弄乱迭代器!)。
它可能看起来像这样:
async function main() {
var result1 = await request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = await request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
main();
如您所见,可以直接调用async function
(如main()
),而无需像runGenerator(..)
或ASQ().runner(..)
类的包装器实用程序runGenerator(..)
进行包装。 在内部,而不是使用yield
,而是使用await
(另一个新的关键字),它告诉async function
在继续操作之前等待promise完成。
基本上,我们将具有库包装生成器的大多数功能,但直接由本机语法支持。
酷吧!?
同时,诸如异步之类的库为我们提供了这些运行程序实用程序,使充分利用异步生成器变得相当容易!
摘要
简而言之:生成器+ yield
ed promise(s)结合了两个方面的优点,从而获得了真正强大而优雅的同步(看上去)异步流控制表达能力。 使用简单的包装器实用程序(许多库已经提供了),我们可以自动运行生成器以完成操作,包括理智和同步(看起来)错误处理!
在ES7 +领域中,我们可能会看到async function
,即使没有库实用程序,它也可以让我们做这些事情(至少对于基本情况而言)!
JavaScript中异步的未来是光明的 ,只会越来越光明! 我得戴上阴影。
但这并没有到此结束。 我们要探索的最后一个视野是:
如果您可以将2个或多个生成器捆绑在一起,让它们独立但“并行”运行,并让它们在进行过程中来回发送消息,该怎么办? 那将是一些超强大的功能,对!!! 这种模式称为“ CSP”(通信顺序过程)。 在下一篇文章中,我们将探索并释放CSP的功能。 请注意!
关于凯尔·辛普森
凯尔·辛普森(Kyle Simpson)是一位开放网络传播者,对JavaScript充满热情。 他是作家,讲习班培训师,技术发言人和OSS贡献者/负责人。
上一篇: ES6生成器的基础
下一篇: ES6(5):迭代器、生成器