原文:https://pouchdb.com/2015/03/0...javascript
PouchDB最棘手的方面之一是它的API是異步的。在Stack Overflow、Github和IRC上,我看到了很多困惑的問題,並且這些問題一般是由對callbacks和promises的誤解形成的。 咱們真的無能爲力。PouchDB是對IndexedDB, WebSQL, LevelDB (in Node), and CouchDB (via Ajax)的抽象。全部這些API都是異步的;所以PouchDB必須是異步的。 然而,當我想到優雅的數據庫API時,我仍然對LocalStorage的簡單性感到震驚: if (!localStorage.foo) { localStorage.foo = 'bar'; }; console.log(localStorage.foo); 要使用LocalStorage,您只需將它看成一個神奇的javascript對象來保存數據。它使用的同步工具集與使用JavaScript自己時已經習慣的工具集相同。 對於LocalStorage的全部錯誤(https://www.html5rocks.com/en/tutorials/offline/quota-research/),這個API的人機工程學在很大程度上解釋了它的持續流行。人們一直在使用LocalStorage,由於它很簡單,並且工做正常。
對於PouchDB,咱們能夠嘗試經過promises來減輕異步API的複雜性,這固然有助於咱們擺脫pyramid of doom。 然而,promisey代碼仍然很難閱讀,由於promisey基本上是語言原語(如try、catch和return)的bolt-on替換: var db = new PouchDB('mydb'); db.post({}).then(function (result) { // post a new doc return db.get(result.id); // fetch the doc }).then(function (doc) { console.log(doc); // log the doc }).catch(function (err) { console.log(err); // log any errors }); 做爲JavaScript開發人員,咱們如今有兩個並行系統——sync and async——咱們必須直截了當地記住這兩個系統。當咱們的控制流變得更復雜時,狀況會變得更糟,咱們須要使用promise.all()和promise.resolve()等API。或者咱們只是選擇了衆多幫助程序庫中的一個,並祈禱咱們可以理解文檔。 直到最近,這是咱們所能指望的最好的。但全部這些都隨ES7而改變。
若是我告訴你,有了ES7,你能夠把上面的代碼改寫成這樣: let db = new PouchDB('mydb'); try { let result = await db.post({}); let doc = await db.get(result.id); console.log(doc); } catch (err) { console.log(err); } 若是我告訴你,多虧了Babel.js和Regenerator這樣的工具,你如今能夠將其發展到ES5並在瀏覽器中運行它了? 女士們先生們,請你們鼓掌,直到博文結束。 首先,讓咱們看看ES7是如何完成這一驚人的壯舉的。
ES7爲咱們提供了一種新的函數,async函數。在async函數內部,咱們有一個新的關鍵字wait,用於「wait for」一個promise: async function myFunction() { let result = await somethingThatReturnsAPromise(); console.log(result); // cool, we have a result } 若是promise resolves,咱們能夠在下一行當即與之交互。若是它拒絕了,那麼就會拋出一個錯誤。因此,try/catch實際上再次有效! async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); // oh noes, we got an error } } 這容許咱們編寫表面上看起來是同步的,但其實是異步的代碼。API返回一個promise 而不是阻塞事件循環這一事實只是一個實現細節。 還記得你何時能夠只使用'return'和'try/catch'嗎? 最好的一點是,咱們今天能夠把它和任何一個能夠返回promises 的庫一塊兒使用。PouchDB就是這樣一個庫,因此讓咱們用它來測試咱們的理論。
首先,考慮一下pouchdb中的一個常見習慣用法:若是文檔存在,咱們但願按_id獲取一個文檔,若是不存在,則返回一個新文檔。 有了promises,你就必須寫下這樣的東西: db.get('docid').catch(function (err) { if (err.name === 'not_found') { return {}; // new doc } throw err; // some error other than 404 }).then(function (doc) { console.log(doc); }) 對於異步函數,這將變成: let doc; try { doc = await db.get('docid'); } catch (err) { if (err.name === 'not_found') { doc = {}; } else { throw err; // some error other than 404 } } console.log(doc); 可讀性更高!若是db.get()直接返回一個文檔而不是一個promise,那麼這幾乎是咱們編寫的代碼。惟一的區別是,當咱們調用任何promise-returning函數時,必須添加wait關鍵字。
我在玩這個的時候遇到了一些微妙的問題,因此很高興能意識到它們。 首先,當您等待某件事情時,您須要在一個async函數中。所以,若是您的代碼嚴重依賴PouchDB,您可能會發現您編寫了許多async函數,但不多有常規函數。 另外一個更陰險的問題是,您必須當心地將代碼包裝在try/catch中,不然promise可能會被拒絕,在這種狀況下,錯誤會被默默地吞沒。(!) 個人建議是確保您的async函數徹底被try/catch包圍,至少在頂層: async function createNewDoc() { let response = await db.post({}); // post a new doc return await db.get(response.id); // find by id } async function printDoc() { try { let doc = await createNewDoc(); console.log(doc); } catch (err) { console.log(err); } }
當涉及到迭代時,Async 函數會變得很是使人印象深入。例如,假設咱們但願將一些文檔按順序插入到數據庫中。也就是說,咱們但願這些promises一個接一個地執行,而不是同時執行。 使用標準的ES6承諾,咱們必須滾動本身的promise鏈: var promise = Promise.resolve(); var docs = [{}, {}, {}]; docs.forEach(function (doc) { promise = promise.then(function () { return db.post(doc); }); }); promise.then(function () { // now all our docs have been saved }); 這是可行的,但確實很難看。這也很容易出錯,由於若是您不當心作了: docs.forEach(function (doc) { promise = promise.then(db.post(doc)); }); 而後promises實際上會同時執行,這可能會致使意想不到的結果。 可是,使用ES7,咱們可使用常規for循環: let docs = [{}, {}, {}]; for (let i = 0; i < docs.length; i++) { let doc = docs[i]; await db.post(doc); } 這個(很是簡潔的)代碼與promise鏈的做用是同樣的!咱們能夠經過如下方式使其更短: let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } 注意,這裏不能使用foreach()循環,若是你天真地寫: let docs = [{}, {}, {}]; // WARNING: this won't work docs.forEach(function (doc) { await db.post(doc); }); 而後Babel.js將失敗,並出現一些不透明的錯誤: Error : /../script.js: Unexpected token (38:23) > 38 | await db.post(doc); | ^ 這是由於在正常函數中不能使用wait。您必須使用async函數。 可是,若是您嘗試使用async函數,那麼您將獲得一個更微妙的錯誤: let docs = [{}, {}, {}]; // WARNING: this won't work docs.forEach(async function (doc, i) { await db.post(doc); console.log(i); }); console.log('main loop done'); 這將編譯,但問題是這將打印出來: main loop done 0 1 2 發生的是,主函數提早退出,由於await實際上在子函數中。此外,這將同時執行每個promise,這不是咱們的預期。 教訓是:在async函數中有任何函數時要當心。wait只會暫停它的父函數,因此檢查它是否在作你認爲它在作的事情。
可是,若是咱們確實但願同時執行多個promises,那麼使用ES7很容易實現這一點。 回想一下,有了ES6 promises,咱們就有了promise.all()。讓咱們使用它從promises數組中返回一個值數組: var docs = [{}, {}, {}]; return Promise.all(docs.map(function (doc) { return db.post(doc); })).then(function (results) { console.log(results); }); 在ES7中,咱們能夠這樣作,這是一種更簡單的方法: let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); 最重要的部分是1)建立promises數組,該數組當即調用全部的promises;2)咱們在主函數中等待這些promises。若是咱們嘗試使用Array.prototype.map,那麼它將沒法工做: let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); // WARNING: this doesn't work let results = promises.map(async function(promise) { return await promise; }); // This will just be a list of promises :( console.log(results); 不起做用的緣由是咱們在等待子函數的內部,而不是主函數。因此在咱們真正等待完成以前,主函數就退出了。 若是您不介意使用promise.all,也可使用它來整理代碼: let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); 若是咱們使用數組壓縮,這看起來可能會更好。然而,規範還不是最終的,因此目前Regenerator不支持它。
ES7仍然很是前沿。Node.js 或 io.js都不支持Async函數,您必須設置一些實驗標誌,甚至讓babel考慮它。正式來講,async/await規範(https://github.com/tc39/ecmascript-asyncawait#status-of-this-proposal)仍處於「建議」階段。 另外,爲了在ES5瀏覽器中工做,您還須要在您的開發代碼中包含Regenerator運行時和ES6 shims。對我來講,這加起來大約60kb,縮小和gzip。對於許多開發人員來講,這實在是太多了。 然而,全部這些新工具都很是有趣,它們描繪了異步庫在陽光明媚的ES7將來的美好圖景。 因此,若是你想本身玩,我已經創建了一個小的演示庫(https://github.com/nolanlawson/async-functions-in-pouchdb)。要開始,只需檢查代碼,運行npm安裝和npm運行build,就能夠了。關於ES7的更多信息,請看JafarHusain的演講。
異步函數是ES7中的一個新概念。它們將咱們丟失的returns和try/catches返回給咱們,並獎勵咱們已經從使用新的IDIOM編寫同步代碼中得到的知識,這些IDIOM看起來很像舊的IDIOM,但性能更高。 最重要的是,async函數使得像PouchDB這樣的API更容易使用。所以,但願這將減小用戶錯誤和混淆,以及更優雅和可讀的代碼。 誰知道呢,也許人們最終會放棄LocalStorage,選擇更現代的客戶端數據庫。