【问题标题】:Using yield to wait until async code is done使用 yield 等待异步代码完成
【发布时间】:2015-07-01 20:59:27
【问题描述】:

我正在尝试学习如何使用生成器和产量,所以我尝试了以下方法,但它似乎不起作用。

我正在使用以下函数,其中包含 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。我到底做错了什么?

【问题讨论】:

  • 您可能需要查看 async 以使异步调用变得井井有条且轻松......
  • 这并不能帮助我了解它是如何工作的......

标签: javascript node.js yield


【解决方案1】:

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

考虑起来可能会令人困惑,但是当您让其产生时,它就像一个返回语句;左侧尚未分配值... ...更重要的是,如果您将 var y = (yield x) + 1; 放入括号中,之前表达式的其余部分... ...所以您返回,+1 被搁置,直到返回一个值。
然后当它到达时(通过.next( ) 传入),计算表达式的其余部分(然后分配到左侧)。

每次调用返回的对象有两个属性{ value: ..., done: false }
value 是您返回/产生的内容,done 是在函数末尾是否命中实际返回语句(包括隐式返回)。

这是可以用来实现异步魔法的部分。

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);
});

没有魔法。
我没有返回一个值,而是返回一个承诺。
当承诺完成时,我将使用.next( result ) 将结果推回,这让我得到了另一个承诺。

当该承诺解决后,我会使用 .next( newResult ) 等将其推回,直到完成。


我们可以做得更好吗?

我们现在知道,我们只是在等待 Promise 解决,然后在迭代器上调用 .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 涵盖了额外的基础(确保所有收益都被视为承诺,因此您不会因传递非承诺值而崩溃......或允许承诺的数组产生,这成为一个承诺,它将返回该 yield 的结果数组......或围绕承诺处理的 try/catch,将错误扔回迭代器......是的,try/catch 与 yield 语句完美配合,做到了方式,感谢迭代器上的 .throw(err) 方法)。

这些东西并不难实现,但它们使示例变得比它需要的更混乱。

这正是 co 或其他一些“协程”或“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));

asyncawait 关键字一起使用,以实现与协程包装的 promise-yielding 生成器相同的功能,而无需所有外部样板文件(最终还具有引擎级优化)。

如果你正在使用像 BabelJS 这样的转译器,你今天可以试试这个。

我希望这会有所帮助。

【讨论】:

  • 我在另一个方法中使用Promise,是不是和这个一样?
  • 没有。产生承诺是您需要在迭代器内部执行的操作,从生成器的实例化返回。 co 接受那些产生的承诺并将它们的值返回到迭代器的下一轮。
【解决方案2】:

yield 和 generators 与异步无关,它们的主要目的是产生可迭代的值序列,就像这样:

function * gen() {
  var i = 0;
  while (i < 10) {
    yield i++;
  }
}

for (var i of gen()) {
  console.log(i);
}

只需调用带有星号的函数(生成器函数)只会创建生成器对象(这就是您在控制台中看到{} 的原因),可以使用next 函数与之交互。

也就是说,您可以将生成器函数用作异步函数的模拟,但您需要一个特殊的运行器,例如 co

【讨论】:

    【解决方案3】:
    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 库是基于回调的,因此它的函数不返回值,所以yield 将返回 null。

    你提到你在其他地方使用了 Promises;我建议看一下bluebird,尤其是它的promisify 功能。 Promisification 反转回调模型,以便回调的参数现在用于解析 promisified 函数。更好的是,promisifyAll 将转换整个基于回调的 API。

    最后,bluebird 也提供协程功能;但是它的协程必须返回承诺。所以,你的代码可以改写如下:

    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)
    

    【讨论】:

    • 这里的第一个看起来像一个使用 mongo API 的非常简洁的示例。您将如何重写它以使数据库连接不在函数内部?即如何等待一件事,然后做下一件?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-16
    • 1970-01-01
    • 2020-10-04
    • 2019-05-31
    • 2021-12-10
    • 2019-06-16
    相关资源
    最近更新 更多