翻译Taming-the-asynchronous-beast-with-ES7

47次阅读

共计 5804 个字符,预计需要花费 15 分钟才能阅读完成。

原文:https://pouchdb.com/2015/03/0…

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,因为它很简单,而且工作正常。

Promises aren’t a panacea

对于 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 而改变。

Enter 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 是如何完成这一惊人的壮举的。

Async functions

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 就是这样一个库,所以让我们用它来测试我们的理论。

Managing errors and return values

首先,考虑一下 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 关键字。

Potential gotchas

我在玩这个的时候遇到了一些微妙的问题,所以很高兴能意识到它们。首先,当您等待某件事情时,您需要在一个 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);
  }
}

Loops

当涉及到迭代时,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 只会暂停它的父函数,所以检查它是否在做你认为它在做的事情。

Concurrent loops

但是,如果我们确实希望同时执行多个 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 不支持它。

Caveats

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 的演讲。

Conclusion

异步函数是 ES7 中的一个新概念。它们将我们丢失的 returns 和 try/catches 返回给我们,并奖励我们已经从使用新的 IDIOM 编写同步代码中获得的知识,这些 IDIOM 看起来很像旧的 IDIOM,但性能更高。最重要的是,async 函数使得像 PouchDB 这样的 API 更容易使用。因此,希望这将减少用户错误和混淆,以及更优雅和可读的代码。谁知道呢,也许人们最终会放弃 LocalStorage,选择更现代的客户端数据库。

正文完
 0