欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

与ES6生成器异步

程序员文章站 2022-07-13 08:42:18
...

与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代码示例也遭受所有相同的“控制反转”问题(又称“回调地狱”)。 到目前为止,我们缺少一些观察结果:

  1. 没有明确的错误处理路径。 正如我们在上一篇文章中了解到的 ,我们可以检测到Ajax调用发生错误(以某种方式),并使用it.throw(..)将其传递回我们的生成器,然后在生成器逻辑中使用try..catch来处理它。 但这只是连接到“后端”(处理生成器迭代器的代码)的更多手动工作,并且如果我们在程序中使用大量生成器,则可能不是可以重复使用的代码。
  2. 如果makeAjaxCall(..)实用程序不在我们的控制之下,并且碰巧多次调用该回调,或者同时发信号通知成功和错误,等等,那么我们的生成器将陷入困境(未捕获的错误,意外值等) 。 处理和预防此类问题需要大量重复的手动工作,也可能不便于携带。
  3. 通常,我们需要“并行”执行多个任务 (例如,同时执行两个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 );
            }
        }
    })();
}

注意的关键事项:

  1. 我们自动初始化发生器(创建其it的迭代器),和我们异步运行it来完成( done:true )。
  2. 我们寻找可以yield的承诺(也就是每次it.next(..)调用的返回value )。 如果是这样,我们通过在promise上注册then(..)来等待它完成。
  3. 如果返回了任何立即值(又名非承诺值),我们只需将该值发送回生成器中,这样它就可以立即运行。

现在,我们如何使用它?

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,我们可以免费获得上述问题的所有这些解决方案:

  1. 现在,我们具有内置的错误处理功能,可以轻松进行连接。 我们上面没有在runGenerator(..)显示它,但是侦听一个it.throw(..)错误并将其连接到它上并不困难it.throw(..) -然后我们可以使用try..catch在我们的生成器代码中捕获和处理错误。
  2. 我们得到了承诺所提供的所有控制/信任 不用担心,不必大惊小怪。
  3. 承诺之上有许多强大的抽象,可以自动处理多个“并行”任务等的复杂性。

    例如, 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 );
    } );
}

好多了 ,不是!?

接着,asynquencerunner(..)的插件消耗发电机右在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的功能。 请注意!

与ES6生成器异步

关于凯尔·辛普森

凯尔·辛普森(Kyle Simpson)是一位开放网络传播者,对JavaScript充满热情。 他是作家,讲习班培训师,技术发言人和OSS贡献者/负责人。

github.com 帖子

翻译自: https://davidwalsh.name/async-generators