使用 yield 等待异步代码完成
Using yield to wait until async code is done
我正在尝试学习如何使用生成器和 yield,所以我尝试了以下方法,但它似乎不起作用。
我正在使用以下函数,其中包含 2 个异步调用:
var client = require('mongodb').MongoClient;
$db = function*(collection, obj){
var documents;
yield client.connect('mongodb://localhost/test', function*(err, db){
var c = db.collection(collection);
yield c.find(obj).toArray(function(err, docs){
documents = docs;
db.close();
});
});
return documents.length;
};
然后调用原始调用,我这样做:
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
var total = $db("ads", {"details.keywords": {$in: query["keywords[]"]}});
console.log(total);
当我在控制台中恢复输出时,我得到了这个:
{}
我期待 200
这样的数字。我做错了什么?
yield 和 generators 与异步无关,它们的主要目的是产生可迭代的值序列,就像这样:
function * gen() {
var i = 0;
while (i < 10) {
yield i++;
}
}
for (var i of gen()) {
console.log(i);
}
只需调用带有星号的函数(生成器函数)只会创建生成器对象(这就是为什么您在控制台中看到 {}
),可以使用 next
函数进行交互。
就是说,您可以将生成器函数用作异步函数的模拟,但您需要一个特殊的运行器,例如 co。
TL;DR
简而言之,您正在寻找像 co 这样的帮手。
var co = require("co");
co(myGen( )).then(function (result) { });
但是为什么呢?
ES6 迭代器或定义它们的生成器没有任何内在的异步性。
function * allIntegers ( ) {
var i = 1;
while (true) {
yield i;
i += 1;
}
}
var ints = allIntegers();
ints.next().value; // 1
ints.next().value; // 2
ints.next().value; // 3
不过,.next( )
方法实际上允许您将 in 中的数据发送回迭代器。
function * exampleGen ( ) {
var a = yield undefined;
var b = yield a + 1;
return b;
}
var exampleIter = exampleGen();
exampleIter.next().value; // undefined
exampleIter.next(12).value; // 13 (I passed 12 back in, which is assigned to a)
exampleIter.next("Hi").value; // "Hi" is assigned to b, and then returned
想起来可能会令人困惑,但是当你让步时,它就像一个 return 语句;左侧还没有被赋值... ...更重要的是,如果你把 var y = (yield x) + 1;
括号解析为 before 表达式的其余部分... ...所以你 return,+1 被搁置,直到返回一个值。
然后当它到达时(通过 .next( )
传入),计算表达式的其余部分(然后分配给左侧)。
从每次调用中 return 编辑的对象有两个属性 { value: ..., done: false }
value
是你所拥有的 returned/yielded 而 done
是它是否在函数末尾命中实际的 return 语句(包括隐式 returns ).
这是可以用来实现这种异步魔法的部分。
function * asyncGen ( id ) {
var key = yield getKeyPromise( id );
var values = yield getValuesPromise( key );
return values;
}
var asyncProcess = asyncGen( 123 );
var getKey = asyncProcess.next( ).value;
getKey.then(function (key) {
return asyncProcess.next( key ).value;
}).then(function (values) {
doStuff(values);
});
没有魔法。
我 return 不是 return 一个值,而是一个承诺。
当承诺完成时,我使用 .next( result )
将结果推回,这让我得到了另一个承诺。
当承诺解决时,我将其推回,使用 .next( newResult )
,等等,直到我完成。
我们可以做得更好吗?
我们现在知道我们只是在等待 promises 解决,然后用结果在迭代器上调用 .next
。
我们是否必须提前知道迭代器的样子,才能知道我们什么时候完成?
不是真的。
function coroutine (iterator) {
return new Promise(function (resolve, reject) {
function turnIterator (value) {
var result = iterator.next( value );
if (result.done) {
resolve(result.value);
} else {
result.value.then(turnIterator);
}
}
turnIterator();
};
}
coroutine( myGen ).then(function (result) { });
这并不完整和完美。 co 涵盖额外的基础(确保所有收益率都像承诺一样对待,所以你不会通过传递非承诺值而爆炸... ...或允许承诺数组被产生,这成为一个承诺,它将 return 该产量的结果数组......或 try/catch 围绕承诺处理,将错误抛回迭代器......是的,try/catch 与 yield 语句完美配合,以这种方式完成,多亏了迭代器上的 .throw(err)
方法)。
这些东西并不难实现,但它们使示例变得比它需要的更加混乱。
这就是为什么 co 或其他一些 "coroutine" 或 "spawn" 方法非常适合这个东西的原因。
Express 服务器背后的人构建了 KoaJS,使用 Co 作为库,Koa 的中间件系统只是在其 .use
方法中使用生成器并做正确的事情。
但是等等,还有更多!
从 ES7 开始,规范很可能会为这个确切的用例添加语言。
async function doAsyncProcess (id) {
var key = await getKeyPromise(id);
var values = await getValuesPromise(key);
return values;
}
doAsyncProcess(123).then(values => doStuff(values));
async
和 await
关键字一起使用,以实现与协程包装的 promise-yielding 生成器相同的功能,无需所有外部样板(并进行引擎级优化,最终)。
如果你使用像 BabelJS.
这样的转译器,你今天就可以试试这个
希望对您有所帮助。
var client = require('mongodb').MongoClient;
$db = function*(collection, obj){
var documents;
yield client.connect('mongodb://localhost/test', function*(err, db){
var c = db.collection(collection);
yield c.find(obj).toArray(function(err, docs){
documents = docs;
db.close();
});
});
return documents.length;
};
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
var total = $db("ads", {"details.keywords": {$in: query["keywords[]"]}});
console.log(total);
照原样,total
是 $db
生成器函数的迭代器。您将通过 total.next().value
检索其 yield
值。但是,mongodb 库是基于回调的,因此,它的函数没有 return 值,因此 yield
将 return 为空。
您提到您在其他地方使用 Promises;我建议看一下 bluebird in particular its promisify functionality. Promisification inverts the callback model so that the arguments to the callback are now used to resolve the promisified function. Even better, promisifyAll will convert an entire callback based API.
最后,bluebird 还提供协程功能;但是它的协程必须 return promises。因此,您的代码可能会重写如下:
var mongo = require('mongodb');
var Promise = require('bluebird');
//here we convert the mongodb callback based API to a promised based API
Promise.promisifyAll(mongo);
$db = Promise.coroutine(function*(collection, obj){
//existing functions are converted to promised based versions which have
//the same name with 'Async' appended to them
return yield mongo.MongoClient.connectAsync('mongodb://localhost/test')
.then(function(db){
return db.collectionAsync(collection);})
.then(function(collection) {
return collection.countAsync();});
});
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
$db('ads',{"details.keywords": {$in: query["keywords[]"]}})
.then(console.log)
我正在尝试学习如何使用生成器和 yield,所以我尝试了以下方法,但它似乎不起作用。
我正在使用以下函数,其中包含 2 个异步调用:
var client = require('mongodb').MongoClient;
$db = function*(collection, obj){
var documents;
yield client.connect('mongodb://localhost/test', function*(err, db){
var c = db.collection(collection);
yield c.find(obj).toArray(function(err, docs){
documents = docs;
db.close();
});
});
return documents.length;
};
然后调用原始调用,我这样做:
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
var total = $db("ads", {"details.keywords": {$in: query["keywords[]"]}});
console.log(total);
当我在控制台中恢复输出时,我得到了这个:
{}
我期待 200
这样的数字。我做错了什么?
yield 和 generators 与异步无关,它们的主要目的是产生可迭代的值序列,就像这样:
function * gen() {
var i = 0;
while (i < 10) {
yield i++;
}
}
for (var i of gen()) {
console.log(i);
}
只需调用带有星号的函数(生成器函数)只会创建生成器对象(这就是为什么您在控制台中看到 {}
),可以使用 next
函数进行交互。
就是说,您可以将生成器函数用作异步函数的模拟,但您需要一个特殊的运行器,例如 co。
TL;DR
简而言之,您正在寻找像 co 这样的帮手。
var co = require("co");
co(myGen( )).then(function (result) { });
但是为什么呢?
ES6 迭代器或定义它们的生成器没有任何内在的异步性。
function * allIntegers ( ) {
var i = 1;
while (true) {
yield i;
i += 1;
}
}
var ints = allIntegers();
ints.next().value; // 1
ints.next().value; // 2
ints.next().value; // 3
不过,.next( )
方法实际上允许您将 in 中的数据发送回迭代器。
function * exampleGen ( ) {
var a = yield undefined;
var b = yield a + 1;
return b;
}
var exampleIter = exampleGen();
exampleIter.next().value; // undefined
exampleIter.next(12).value; // 13 (I passed 12 back in, which is assigned to a)
exampleIter.next("Hi").value; // "Hi" is assigned to b, and then returned
想起来可能会令人困惑,但是当你让步时,它就像一个 return 语句;左侧还没有被赋值... ...更重要的是,如果你把 var y = (yield x) + 1;
括号解析为 before 表达式的其余部分... ...所以你 return,+1 被搁置,直到返回一个值。
然后当它到达时(通过 .next( )
传入),计算表达式的其余部分(然后分配给左侧)。
从每次调用中 return 编辑的对象有两个属性 { value: ..., done: false }
value
是你所拥有的 returned/yielded 而 done
是它是否在函数末尾命中实际的 return 语句(包括隐式 returns ).
这是可以用来实现这种异步魔法的部分。
function * asyncGen ( id ) {
var key = yield getKeyPromise( id );
var values = yield getValuesPromise( key );
return values;
}
var asyncProcess = asyncGen( 123 );
var getKey = asyncProcess.next( ).value;
getKey.then(function (key) {
return asyncProcess.next( key ).value;
}).then(function (values) {
doStuff(values);
});
没有魔法。
我 return 不是 return 一个值,而是一个承诺。
当承诺完成时,我使用 .next( result )
将结果推回,这让我得到了另一个承诺。
当承诺解决时,我将其推回,使用 .next( newResult )
,等等,直到我完成。
我们可以做得更好吗?
我们现在知道我们只是在等待 promises 解决,然后用结果在迭代器上调用 .next
。
我们是否必须提前知道迭代器的样子,才能知道我们什么时候完成?
不是真的。
function coroutine (iterator) {
return new Promise(function (resolve, reject) {
function turnIterator (value) {
var result = iterator.next( value );
if (result.done) {
resolve(result.value);
} else {
result.value.then(turnIterator);
}
}
turnIterator();
};
}
coroutine( myGen ).then(function (result) { });
这并不完整和完美。 co 涵盖额外的基础(确保所有收益率都像承诺一样对待,所以你不会通过传递非承诺值而爆炸... ...或允许承诺数组被产生,这成为一个承诺,它将 return 该产量的结果数组......或 try/catch 围绕承诺处理,将错误抛回迭代器......是的,try/catch 与 yield 语句完美配合,以这种方式完成,多亏了迭代器上的 .throw(err)
方法)。
这些东西并不难实现,但它们使示例变得比它需要的更加混乱。
这就是为什么 co 或其他一些 "coroutine" 或 "spawn" 方法非常适合这个东西的原因。
Express 服务器背后的人构建了 KoaJS,使用 Co 作为库,Koa 的中间件系统只是在其 .use
方法中使用生成器并做正确的事情。
但是等等,还有更多!
从 ES7 开始,规范很可能会为这个确切的用例添加语言。
async function doAsyncProcess (id) {
var key = await getKeyPromise(id);
var values = await getValuesPromise(key);
return values;
}
doAsyncProcess(123).then(values => doStuff(values));
async
和 await
关键字一起使用,以实现与协程包装的 promise-yielding 生成器相同的功能,无需所有外部样板(并进行引擎级优化,最终)。
如果你使用像 BabelJS.
这样的转译器,你今天就可以试试这个希望对您有所帮助。
var client = require('mongodb').MongoClient;
$db = function*(collection, obj){
var documents;
yield client.connect('mongodb://localhost/test', function*(err, db){
var c = db.collection(collection);
yield c.find(obj).toArray(function(err, docs){
documents = docs;
db.close();
});
});
return documents.length;
};
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
var total = $db("ads", {"details.keywords": {$in: query["keywords[]"]}});
console.log(total);
照原样,total
是 $db
生成器函数的迭代器。您将通过 total.next().value
检索其 yield
值。但是,mongodb 库是基于回调的,因此,它的函数没有 return 值,因此 yield
将 return 为空。
您提到您在其他地方使用 Promises;我建议看一下 bluebird in particular its promisify functionality. Promisification inverts the callback model so that the arguments to the callback are now used to resolve the promisified function. Even better, promisifyAll will convert an entire callback based API.
最后,bluebird 还提供协程功能;但是它的协程必须 return promises。因此,您的代码可能会重写如下:
var mongo = require('mongodb');
var Promise = require('bluebird');
//here we convert the mongodb callback based API to a promised based API
Promise.promisifyAll(mongo);
$db = Promise.coroutine(function*(collection, obj){
//existing functions are converted to promised based versions which have
//the same name with 'Async' appended to them
return yield mongo.MongoClient.connectAsync('mongodb://localhost/test')
.then(function(db){
return db.collectionAsync(collection);})
.then(function(collection) {
return collection.countAsync();});
});
var qs = require("querystring");
var query = qs.parse("keywords[]=abc&keywords[]=123");
$db('ads',{"details.keywords": {$in: query["keywords[]"]}})
.then(console.log)