原文: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,选择更现代的客户端数据库。