您的位置:首页 > Web前端 > JavaScript

使用JavaScript生成器解决回调问题的研究

2017-03-17 17:25 781 查看
当我第一次开始编写node.js代码时,有两件事我讨厌:all the popular templating engines和the proliferation of callbacks。我愿意忍受回调,因为我理解基于事件的服务器的能力,但是后来我看到JavaScript生成器的到来,我迫切地期待它实施的那一天。

现在,那一天到来了。他们已经部署在 landed in V8,并且SpiderMonkey的implementation也正在被更新到规范中。

虽然V8隐藏了和谐特性,如命令行标志后面的生成器,并且它在所有浏览器中可用之前还有一段时间(即使Firefox已经永久存在),我们可以继续研究如何使用生成器写入异步代码。我们应该早期建立这些模式。

你今天可以通过下载节点的不稳定版本0.11

那么生成器如何帮助避免节点的回调地狱?生成器函数可以使用yield关键字暂停执行,并在恢复和挂起时来回传递值。这意味着我们可以“暂停”一个函数,当它需要等待一个函数的结果,而不是传递一个回调。

更新:一定要阅读我的后续帖子

Generator 基础

让我们看看一个原始的生成器函数,然后再进入异步。生成器的定义为function*:



我不打算深入这个太多,因为我想专注于如何使用这个与异步构造。以下是使用生成器的方法:



如果我在课堂上做笔记,我会写下来:

·yield允许存在在表达式的任何地方。所以在任何事件(例如foo(yield x, yield y), 或者 loops)的中间暂停函数,这使它成为一个强大的结构。
·虽然调用生成器看起来像一个function,但它只是创建一个生成器对象。您需要调用next或send启动生成器。当想要将值发送回它时使用send。gen.next()相当于gen.send(null)。还有一个gen.throw,它在生成器中抛出异常。
·生成器方法不返回原始值,它们返回具有两个属性的对象:value和done。明确了一个生成器的完成,而不是再用return或者函数的结束,取代旧的API中的a clunky StopIteration exception。

异步解决方案#1:挂起

上面的代码与节点的回调地狱有什么关系?好吧,如果我们能够任意地暂停函数的执行,我们可以将异步回调代码转换回一丢丢糖的同步代码。

问题是:糖是什么?

建议的第一个解决方案是挂起的库(the suspend library)。这是我们可以达到的最简单的。说真的,它只有16行代码

下面是with这个库的异步代码:



该suspend函数将你的generator转换为一个generator运行的正常函数。它将一个resume函数传递给生成器,该函数应该被用作所有异步函数的回调,并且它将用一个包含错误和值的2-element数组来恢复生成器。

resume和generator之间的互动是有趣的,但它有一些缺点。首先,获取一个2-element数组,即使使用destructuring(var [err, res] = yield foo(resume))也是令人讨厌的。我宁愿它只返回value,并且在错误存在时将错误作为一场抛出。它看起来像一个作为支持错误可选项的库,但我认为它应该被默认。

其次,总是要有明确传递resume有点尴尬,它不是非常composable,因为如果我想等到上一个的函数完成,我还需要添加一个callback参数,并且在函数的结束的时候调用它像你通常在节点做的那样。这会导致错误处理更加严重,因为错误需要向前传递而不是抛出,因此您需要在每个异步调用函数中手动检查和转发错误。

最后,你不能做更复杂的控制流,如并行多个事情。该自述称,其他控制流库已经解决这个问题,你应该在使用suspend的同时使用它们其中的一个,但我宁愿看到控制流程库结合生成原生支持。

更新:creationix写的 kriskowal提到了这个要领 ,它实现了基于回调的代码更好的独立发电处理程序。这是很酷,默认情况下抛出错误,是更干净。

异步解决方案#2:Promises

一个更好的办法来处理异步流程就是使用Promises。promise是一个表示未来值的对象,您可以撰写promise来表示涉及异步行为的程序的控制流。

我不会在这里解释Promises,因为它需要太长时间,并且已经有了一个很好的解释。最近,很多重点放在定义promise的行为和API以允许库之间的互操作,但是这个想法很简单。

我将使用Q promise库,因为它已经对generator有了初步支持,也很成熟。task.js是这个想法的早期实现,但它有一个非标准的promise实现。

让我们退一步,看看一个现实中的例子。此代码创建一个post,然后获取它,并获取具有相同标签的post(client是一个redis实例):

client.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
}, function(err, res) {
if(err) throw err;

client.hgetall('blog::post', function(err, post) {
if(err) throw err;

var tags = post.tags.split(',');
var posts = [];

tags.forEach(function(tag) {
client.hgetall('post::tag::' + tag, function(err, taggedPost) {
if(err) throw err;
posts.push(taggedPost);

if(posts.length == tags.length) {
// do something with post and taggedPosts

client.quit();
}
});
});

});
});


这不是一个复杂的例子,但是代码略冗杂。回调将代码很长。此外,要查询所有标记,我们需要手动管理每个查询,并检查所有标记是否已准备就绪。

让我们把它变成Q promises

var db = {
get: Q.nbind(client.get, client),
set: Q.nbind(client.set, client),
hmset: Q.nbind(client.hmset, client),
hgetall: Q.nbind(client.hgetall, client)
};

db.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
}).then(function() {
return db.hgetall('blog::post');
}).then(function(post) {
var tags = post.tags.split(',');

return Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
})).then(function(taggedPosts) {
// do something with post and taggedPosts

client.quit();
});
}).done();

我们不得不将基于回调的redis函数包装为基于promise的函数,但这很简单。一旦我们有promises,你调用then等待异步操作的结果。更详细的解释是在promise / A + spec中。

Q实现了一些额外的方法,如all,它接受一个promise 数组,并等待所有的完成。还有done,它意味着,你的异步工作流完成和任何未处理的错误应抛出。根据promises / A + spec,所有的异常都被转换为错误并传递给错误处理程序,所以你需要确保如果不处理它们就重新抛出。(如果这让你感到困惑,请阅读Domenic的这篇博文。)

注意我们不得不嵌套最终promise处理程序,因为我们需要访问post以及taggedPosts。这感觉类似于回调样式代码,这是不幸的。

现在,是时候探索generator的力量:
Q.async(function*() {
yield db.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
});

var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');

var taggedPosts = yield Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
}));

// do something with post and taggedPosts

client.quit();
})().done();是不是很神奇?那么这里到底发生了啥呢?

Q.async需要一个generator并返回一个运行它的函数,很像suspend库。然而,有一个关键的区别,generator会yield一些promises。Q接受每个promise并将generator绑定到它,使得当promise被满足时恢复,并发送回结果。

我们不必处理笨重的resume函数,promise被隐式处理,我们从所有的promise行为中受益。

其中一个好处是,我们可以在我们需要时再使用Q promises ,例如,Q.all,它能并行运行几个异步操作。这样,很容易将显式Q promises和隐式promises组合在generator中来创建看起来很干净的复杂流。

还要注意,我们根本没有嵌套问题。既然post和taggedPosts保持在同样的范围,我们不必再关心then链断裂范围,这是令人难以置信的棒。

错误处理有点棘手,你真的应该在使用之前了解promise如何在generator中工作。promise中的错误和异常总是传递给错误处理函数,而永不会被抛出。一个async generator是一个promise,不是异常。您可以使用错误回调处理错误:someGenerator().then(null, function(err) { ... })。

然而,generator promises有一个特殊的行为是它的promises中的任何错误将使用特殊的gen.throw方法从generator抛出,它从被generator中断的点抛出一个异常。这意味着您可以使用try/ catch来处理生成器中的错误:

Q.async(function*() {
try {
var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');

var taggedPosts = yield Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
}));

// do something with post and taggedPosts
}
catch(e) {
console.log(e);
}

client.quit();
})();它工作的方式是你所期望的; 来自任何db.hgetall函数的错误将在处理catch程序中处理,即使错误可能来自其中的嵌套在Q.all中的promise。当然,如果没有try/ catch,异常会被转换回一个错误,并传递给调用promise的错误处理程序(不符合上面任意一条,所以错误会被安静地抑制)。

 让我们先不考虑上一条。我们可以安装异常处理程序的try / catch异步代码。并且错误处理程序的动态范围正常工作; 在try块执行时发生的任何未处理的错误将被给予catch。你甚至可以使用finally来确保“clearup”代码是运行,即使有错误,而不必处理错误。

此外,只要你使用promises时也调用done,你也要默认将得到错误抛出,而不是静静地忽略,这种情况常常发生异步代码。使用的方式Q.async通常看起来像这样:

var getTaggedPosts = Q.async(function*() {
var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');

return Q.all(tags.map(function(tag) {
return db.hget('blog::tag::' + tag);
}));
});上面是库代码,只是简单地创建promise,不关心错误处理。你可以这样调用:
Q.async(function*() {
var tagged = yield getTaggedPosts();
// do something with the tagged array
})().done();
这是top-level代码。如前所述,该done方法确保将任何未处理的错误抛出作为异常。我相信上述模式太常见以至于我们需要一个新的方法。该getTaggedPosts是一个库函数,被用来作为一个promise generator函数。上面的代码只是consumes promises的top-level代码。

我在这个pull请求中提出Q.spawn,并且它已经合并到Q了!这使得简单地运行消耗承诺的代码变得更简单:

Q.spawn(function*() {
var tagged = yield getTaggedPosts();
// do something with the tagged array
});spawn take了一个generator并立即运行了它,并自动重新抛出任何未处理的错误。它完全相当于Q.done(Q.async(function*() { ... })())。

其他模式

我们的promised-based generator代码开始形成。有了这一点语法糖,我们可以清除许多通常由异步工作流造成的冗余。

使用generator工作了一段时间后,这里有几个我注意到的模式。

不值得

如果你有一个短的函数,它只需要等待一个promise,那它是不值得创建一个generator的。比较此代码:

var getKey = Q.async(function*(key) {
var x = yield r.get(dbkey(key));
return x && parseInt(x, 10);
});和这个代码:
function getKey(key) {
return r.get(dbkey(key)).then(function(x) {
return x && parseInt(x, 10);
});
}我认为后者看起来更干净。
spawnMap

这是我发现自己做的过多的一部分

yield Q.all(keys.map(Q.async(function*(dateKey) {
var date = yield lookupDate(dateKey);
obj[date] = yield getPosts(date);
})));它可能是有帮助的spawnMap,它Q.all(arr.map(Q.async(...)))为你执行。

yield spawnMap(keys, function*(dateKey) {
var date = yield lookupDate(dateKey);
obj[date] = yield getPosts(date);
})));类似于异步库中的map方法

asyncCallback

我注意到的最后一件事是,有时,我想创建一个Q.async函数,但我想强制将所有的错误重新抛出。这种情况发生在各库的正常回调,如express:app.get('/url', function() { ... })。

我不能将上述回调转换为一个Q.async函数,因为所有的错误将被安静地抑制,但我不能使用Q.spawn,因为它不应该被立即执行。也许像asyncCallback就是有顺序的:
function asyncCallback(gen) {
return function() {
return Q.async(gen).apply(null, arguments).done();
};
}

app.get('/project/:name', asyncCallback(function*(req, res) {
var counts = yield db.getCounts(req.params.name);
var post = yield db.recentPost();

res.render('project.html', { counts: counts,
post: post });
}));

最后的想法

当我发现generator,我它帮助实现异步代码抱很大的希望。事实证明,他们做到了,虽然你真的不得不了解promises如何工作,以有效地将它们与generator组合。使promises含蓄使他们有点神秘,所以我不会使用async或spawn直到你明白一般的promise。

尽管我希望JavaScript神奇地具有延续性,以便所有这些都可由语言隐式处理,这个解决方案相当不错。我们现在有一个简洁的异步行为编码方式,这是非常强大的,因为它不仅仅是使文件系统操作更漂亮。我们本质上有一种方法来编写简洁的分布式代码,可以跨进程,甚至机器操作,同时看起来同步。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  JavaScript Generator
相关文章推荐