2015-04-23 21 views
2

我想學習如何使用生成器和yield,所以我嘗試了以下,但它似乎並沒有工作。使用良率等到異步代碼完成

我使用下面的函數,其中包含2個異步調用:當我得到我的輸出回

var qs = require("querystring"); 

var query = qs.parse("keywords[]=abc&keywords[]=123"); 
var total = $db("ads", {"details.keywords": {$in: query["keywords[]"]}}); 
console.log(total); 

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

然後撥打電話原來的呼叫,我這樣做在控制檯,我得到這個:

{} 

我期待一個數字,如200。我做錯了什麼?

+0

你可能想看看[異步](https://github.com/caolan/async)作出異步調用井井有條,方便... – DrCord

+1

那將不會幫助我瞭解這是如何工作的...... –

回答

7

TL; DR

對於簡短的回答,你正在尋找一個幫手像合作

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()方法,但是,實際上可以讓你在數據發送回迭代器。

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;的括號解決了之前其餘的表達......所以你返回,並且+1被保留,直到值返回。
然後當它到達時(通過.next()傳入),評估其餘表達式(然後分配到左側)。

這與每個調用返回的對象有兩個屬性{ value: ..., done: false }
value是你返回/屈服,done是,是否它擊中了實際return語句在函數結束時(包括隱性收益)是什麼。

這是可以用來使這種異步魔術發生的部分。

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),等等,直到我完成。


我們可以做得更好嗎?

我們現在知道,我們只是在等待解決的承諾,然後在結果的迭代器上調用.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涵蓋了額外的基礎(確保所有的收益都像承諾一樣被對待,所以你不會通過傳遞一個非承諾的價值來炸燬......或者允許承諾的數組被放棄,這成爲一個承諾將返回該結果的數組......或嘗試/捕捉承諾處理,將錯誤返回到迭代器中......是的,嘗試/ catch完美地與yield語句完美結合,這樣做,這要歸功於在迭代器上的.throw(err)方法)。

這些東西並不難實現,但它們使得這個例子比需要的更混亂。

這正是爲什麼co或其他「協同程序」或「產卵」方法是完美的這個東西。

快速服務器背後的傢伙構建了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關鍵字一起使用時,以實現相同的功能的協程包裹承諾高產發生器,沒有所有的外部樣板的(並與發動機級優化,最終)。

如果您使用像BabelJS這樣的轉譯器,您可以今天試試。

我希望這會有所幫助。

+0

我在另一種方法中使用'Promise',是做同樣的事情嗎? –

+0

編號承諾是您在迭代器內部需要做的事情,從生成器的實例化返回。 'co'接受那些產生的承諾,並將它們的值返回到迭代器的下一輪。 – Norguard

1

產量和發電無關與異步,他們的主要目的是產生價值的迭代序列,就像這樣:

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

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

只需調用帶有星號(發生器功能)功能只會製造發生器對象(這就是爲什麼你在控制檯中看到{}),可以使用next函數進行交互的原因。這就是說,你可以使用發電機功能作爲異步功能的模擬,但是你需要一個特殊的轉輪,如co

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

AS是,total對於$db發生器功能的迭代器。您將通過total.next().value檢索其yield值。但是,mongodb庫是基於回調的,因此它的函數不會返回值,因此yield將返回null。

你提到你在別處使用Promise;我建議看看bluebird,特別是它的promisify功能。 Promisification反轉回調模型,以便現在使用回調的參數來解析promisified函數。更好的是,promisifyAll將轉換基於整個回調的API。

最後,藍鳥也提供了協同功能;然而它的協程必須返回承諾。因此,如下代碼可被改寫:

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) 
+1

這裏的第一個看起來像使用mongo API的非常簡潔的示例。你將如何重寫,以便使數據庫連接不在函數內部?即如何等待一件事,然後做下一件事? – dcsan