掌握 Node.js 中的 async/await

kfzs7850 3年前
   <p>你会在本文中学到如何使用 async 函数(async/await) 来简化回调,以及基于 Promise 的 Node.js 应用。</p>    <p>异步语言结构已经在其它语言中存在好一阵了,比如 C# 的 async/await,Kotlin 的协程(Coroutine) 以及 Go 中的 Goroutine。随着 Node.js 8 的发布,期待已久的异步函数功能终于来临。</p>    <p>在本教程结束的时候,你应该可以回答下面的问题:</p>    <p><a href="/misc/goto?guid=4959751903234660450" rel="nofollow,noindex">Node.js 中的 async/await 是自发明面包切片以来最美好的事情吗?</a></p>    <h2>Node 中的异步函数是什么鬼?</h2>    <p>异步函数声明返回 AsyncFunction 对象。这在某种意义上来说与 Generator 相似——它们的执行可以被中止。唯一的不同之处在于他们异步函数总是返回 Promise 而不是 { value: any, done: Boolean } 对象。实际上,异步函数与你从 <a href="/misc/goto?guid=4958854327280779367" rel="nofollow,noindex">co</a> 包中体验到的功能非常相似。</p>    <p>在异步函数中你可以等待(await) 任何 Promise 或捕获其拒绝(reject) 的原因。</p>    <p>因此,如果你有像下面这们用 Promise 实现的逻辑:</p>    <p>〔译者注:文本中的代码都根据译者推荐的格式进行了格式化,这不影响代码的意义〕</p>    <pre>  <code class="language-javascript">function handler(req, res) {      return request("https://user-handler-service")          .catch((err) => {              logger.error("Http error", err);              error.logged = true;              throw err;          })          .then((response) => Mongo.findOne({ user: response.body.user }))          .catch((err) => {              !error.logged && logger.error("Mongo error", err);              error.logged = true;              throw err;          })          .then((document) => executeLogic(req, res, document))          .catch((err) => {              !error.logged && console.error(err);              res.status(500).send();          });  }</code></pre>    <p>你可以使用 async/await 将其修改得更像在编写同步代码:</p>    <pre>  <code class="language-javascript">async function handler(req, res) {      let response;      try {          response = await request("https://user-handler-service");      } catch (err) {          logger.error("Http error", err);          return res.status(500).send();      }        let document;      try {          document = await Mongo.findOne({ user: response.body.user });      } catch (err) {          logger.error("Mongo error", err);          return res.status(500).send();      }        executeLogic(document, req, res);  }</code></pre>    <p>在较旧的 V8 中,如果没有处理拒绝的 Promise,它只是悄悄地被丢弃了。现在至少你会从 Node 中得到一个警告,因此你不必为此担心而去创建一个监听程序。然而,如果你不能处理错误而让应用处于一个未知的状态,那么推荐的做法是让你的应用崩溃掉:</p>    <pre>  <code class="language-javascript">process.on("unhandledRejection", (err) => {      console.error(err);      process.exit(1);  });</code></pre>    <h2>使用异步函数的模式</h2>    <p>确实有一些案例,如果能像写同步程序一样方便的进行异步操作就好了。使用 Promise 和回调来处理它们会需要复杂的模式或引用其它库。</p>    <p>这里有一些例子,需要在循环中异步获取数据,或使用 if-else 条件。</p>    <h3>通过指数补偿进行重试</h3>    <p>使用 Promise 实现重试逻辑非常笨拙:</p>    <pre>  <code class="language-javascript">function requestWithRetry(url, retryCount) {      if (retryCount) {          return new Promise((resolve, reject) => {              const timeout = Math.pow(2, retryCount);                setTimeout(() => {                  console.log("Waiting", timeout, "ms");                  _requestWithRetry(url, retryCount)                      .then(resolve)                      .catch(reject);              }, timeout);          });      } else {          return _requestWithRetry(url, 0);      }  }    function _requestWithRetry(url, retryCount) {      return request(url, retryCount)          .catch((err) => {              if (err.statusCode && err.statusCode >= 500) {                  console.log("Retrying", err.message, retryCount);                  return requestWithRetry(url, ++retryCount);              }              throw err;          });  }    requestWithRetry("http://localhost:3000")      .then((res) => {          console.log(res);      })      .catch(err => {          console.error(err);      });</code></pre>    <p>这个代码看着就头痛。我们可以使用 async/await 来重写这段代码,这样的代码会简单得多。</p>    <pre>  <code class="language-javascript">function wait(timeout) {      return new Promise((resolve) => {          setTimeout(() => {              resolve();          }, timeout);      });  }    async function requestWithRetry(url) {      const MAX_RETRIES = 10;      for (let i = 0; i <= MAX_RETRIES; i++) {          try {              return await request(url);          } catch (err) {              const timeout = Math.pow(2, i);              console.log("Waiting", timeout, "ms");              await wait(timeout);              console.log("Retrying", err.message, i);          }      }  }</code></pre>    <p>这样的代码更让人赏心悦目,不是吗?</p>    <h2>中间值</h2>    <p>这个例子不像之前的例子那么可怕,但是如果你有 3 个异步函数在顺序上存在依赖关系,你恐怕不得不从几个不太好看的解决办法中选择一个。</p>    <p>functionA 返回 Promise,而调用 functionB 需要那个值,然后 functionC 需要从 functionA 和 functionB 的 Promise 中带回来的值。</p>    <p>办法 1: .then 圣诞树</p>    <pre>  <code class="language-javascript">function executeAsyncTask() {      return functionA()          .then((valueA) => {              return functionB(valueA)                  .then((valueB) => {                      return functionC(valueA, valueB);                  });          });  }</code></pre>    <p>在这个办法中,我们通过 3 层代码获得 valueA ,同时从上一个 Promise 获得 valueB 。我们不能让这个圣诞树扁平化,否则就不会形成闭包,在调用 functionC 的时候就拿不到 valueA 。</p>    <p>〔译者注:翻译过程中去掉了一些疑似广告的链接〕</p>    <p>办法 2:移到上层作用域</p>    <pre>  <code class="language-javascript">function executeAsyncTask() {      let valueA;      return functionA()          .then((v) => {              valueA = v;              return functionB(valueA);          })          .then((valueB) => {              return functionC(valueA, valueB);          });  }</code></pre>    <p>在圣诞树中,我们使用了更高导的作用域使 valueA 有效。这里的情况类似,只不过我们现在把 valueA 定义在所有 .then 之外,然后我们可以将第一个 Promise 的确定值赋给它。</p>    <p>这个办法当然有效,即扁平化了 .then 链又保持了正确的语义。然而,它也带来了新的缺陷,比如 valueA 会在函数其它地方使用。我们需要使用两个变量 —— valueA 和 v —— 它们是同一个值。</p>    <p>办法 3:不必要的数组</p>    <pre>  <code class="language-javascript">function executeAsyncTask() {      return functionA()          .then(valueA => {              return Promise.all([valueA, functionB(valueA)]);          })          .then(([valueA, valueB]) => {              return functionC(valueA, valueB);          });  }</code></pre>    <p>把 valueA 与 functionB 产生的 Promise 一起放在数组中,当然是为了使树扁平化。它们可能是完全不同的类型,所以它们根本不应该放在一个数组中的可能性非常大。</p>    <p>办法 4:写一个辅助函数</p>    <pre>  <code class="language-javascript">const converge = (...promises) => (...args) => {      let [head, ...tail] = promises;      if (tail.length) {          return head(...args)              .then((value) => converge(...tail)(...args.concat([value])));      } else {          return head(...args);      }  };    functionA(2)      .then((valueA) => converge(functionB, functionC)(valueA));</code></pre>    <p>你当然可以耍点小聪明,写一个辅助函数来隐藏上下文,但这样的代码会非常难读,对于那些并不精通函数式编程技法的人来说,可能不太容易理解。</p>    <p>使用 async/await 就什么问题都没有了:</p>    <pre>  <code class="language-javascript">async function executeAsyncTask() {      const valueA = await functionA();      const valueB = await functionB(valueA);      return function3(valueA, valueB);  }</code></pre>    <h2>使用 async/await 处理多个并行请求</h2>    <p>这与前面的例子相似。这里你是想同时执行几个异步任务,并不同的地方使用它们的结果值,使用 async/await 很容易解决:</p>    <pre>  <code class="language-javascript">async function executeParallelAsyncTasks() {      const [valueA, valueB, valueC]          = await Promise.all([              functionA(),              functionB(),              functionC()          ]);      doSomethingWith(valueA);      doSomethingElseWith(valueB);      doAnotherThingWith(valueC);  }</code></pre>    <p>正如我们在这个示例中看到的,我们不需要把这些值移到上层作用域,也不需要创建毫无语义的数组来传递这些值。</p>    <h2>数组迭代方法</h2>    <p>你可以结合异步函数使用 map 、 filter 和 reduce ,不过它们的行为并不直观。猜猜下面的脚本会在控制台打印出什么:</p>    <ol>     <li><em>map</em></li>    </ol>    <pre>  <code class="language-javascript">function asyncThing(value) {      return new Promise((resolve, reject) => {          setTimeout(() => resolve(value), 100);      });  }    async function main() {      return [1, 2, 3, 4].map(async (value) => {          const v = await asyncThing(value);          return v * 2;      });  }    main()      .then(v => console.log(v))      .catch(err => console.error(err));</code></pre>    <ol>     <li><em>filter</em></li>    </ol>    <pre>  <code class="language-javascript">function asyncThing(value) {      return new Promise((resolve, reject) => {          setTimeout(() => resolve(value), 100);      });  }    async function main() {      return [1, 2, 3, 4].filter(async (value) => {          const v = await asyncThing(value);          return v % 2 === 0;      });  }    main()      .then(v => console.log(v))      .catch(err => console.error(err));</code></pre>    <ol>     <li><em>reduce</em></li>    </ol>    <pre>  <code class="language-javascript">function asyncThing(value) {      return new Promise((resolve, reject) => {          setTimeout(() => resolve(value), 100);      });  }    async function main() {      return [1, 2, 3, 4].reduce(async (acc, value) => {          return await acc + await asyncThing(value);      }, Promise.resolve(0));  }    main()      .then(v => console.log(v))      .catch(err => console.error(err));</code></pre>    <p>答案:</p>    <pre>  <code class="language-javascript">[ Promise { }, Promise { }, Promise { }, Promise { } ]  [ 1, 2, 3, 4 ]  10  </code></pre>    <p>如果你记录 <strong> map </strong> 迭代过程中的返回值,你会看到我们期望的数组: [ 2, 4, 6, 8 ] 。唯一的问题在于,每个都被 AsyncFunction 封装成了 Promise。</p>    <p>因此,如果你想得到正确的值,就需要用 Promise.all 对返回的数组进行解封。</p>    <pre>  <code class="language-javascript">main()      .then(v => Promise.all(v))      .then(v => console.log(v))      .catch(err => console.error(err));</code></pre>    <p>本来,你应该先等待 Promise 确定值,然后映射值:</p>    <pre>  <code class="language-javascript">function main() {      return Promise.all([1, 2, 3, 4]          .map((value) => asyncThing(value)));  }    main()      .then(values => values.map((value) => value * 2))      .then(v => console.log(v))      .catch(err => console.error(err));</code></pre>    <p>这样看起来简单一些,不是吗?</p>    <p>如果在一个长时间运行的异步任务中,需要迭代一些一些长时间运行的同步逻辑,那么 async/await 仍然会非常有用。</p>    <p>这样一来,只要有一个值你就可以开始计算 —— 不必等到所有 Promise 都解决了才开始计算。即使结果仍然被封装在 Promise 中,它们仍然比顺序执行快得多。</p>    <p>filter 呢?很明显没对...</p>    <p>很好,你猜到了:虽然返回值是 [ false, true, false, true ] ,但它们被封装成 Promise,所以会从原来的数组取回所有值〔译者注:因为 Promise 对象会被判为 true 〕。很不幸,要修正这个错误,就需要要确定所有值,然后再过滤。</p>    <p>〔译者注:译者饶有兴趣的来补充了一个实现,貌似不简单〕</p>    <pre>  <code class="language-javascript">// 译者补充的实现 (只修改了 main 函数)  async function main() {      const promises = [1, 2, 3, 4]          .map(async value => {              const v = await asyncThing(value);              return {                  value: value,                  predicate: v % 2 === 0              };          });        return (await Promise.all(promises))          .filter(m => m.predicate)          .map(m => m.value);  }</code></pre>    <p>归约(reduce)相当直接。但要记住,你需要用 Promise.resolve 封装初始值,返回的积累值也会被封装,需要等待( await )。</p>    <p><a href="/misc/goto?guid=4959751903354938003" rel="nofollow,noindex">如果你希望使用函数式编程模式, async/await 可能不适合你。 </a></p>    <p>.. 因为它的意图明确地用于命令式编程模式。</p>    <p>为了让 .then 链看起来更纯粹,你可以使用 Ramda 的 <a href="/misc/goto?guid=4959751903440037772" rel="nofollow,noindex"> pipeP </a> 和 <a href="/misc/goto?guid=4959751903537634242" rel="nofollow,noindex"> composeP </a> 函数.</p>    <h2>重写基于回调的 Node.js 应用程序</h2>    <p>异步函数默认返回 Promise ,所以你可以重写基于回调的函数,让它们使用 Promise,然后等待( await ) 解决。你可以使用 Node.js 的 <a href="/misc/goto?guid=4959751903617759823" rel="nofollow,noindex"> util.promisify </a> 函数将基于回调的函数转换为基于 Promise 的函数。</p>    <h2>重写基于 Promise 的应用程序</h2>    <p>简单的 .then 链可以直接升级,所以你立刻就能使用 async/await 。</p>    <pre>  <code class="language-javascript">function asyncTask() {      return functionA()          .then((valueA) => functionB(valueA))          .then((valueB) => functionC(valueB))          .then((valueC) => functionD(valueC))          .catch((err) => logger.error(err));  }</code></pre>    <p>可以改为</p>    <pre>  <code class="language-javascript">async function asyncTask() {      try {          const valueA = await functionA();          const valueB = await functionB(valueA);          const valueC = await functionC(valueB);          return await functionD(valueC);      } catch (err) {          logger.error(err);      }  }</code></pre>    <h2>使用 async/await 重写 Node.js 应用</h2>    <ul>     <li> <p>如果你喜欢经典的 if-else 条件和 for/while 循环,</p> </li>     <li> <p>如果你认同 try-catch 块处理错误的方式,</p> </li>    </ul>    <p>你会很愉快的使用 async/await 来改写服务。</p>    <p>正如我们看到的那样,它可以让某些模式更容易编写也更容易阅读,所以它肯定在某些情况下比 Promise.then() 链更合适。然而,如果你陷入了过去几年的函数式编程热,你可能想忽略这一语言特性。</p>    <p>那么你们都在想什么呢? async/await 是发明面包切片之后最好的事情,还是像 es2015 的 class 一样有争议呢?</p>    <p>你是否已经在生产中使用 async/await ,或者准备坚决不会碰它? 让我们在下面的评论中讨论吧。</p>    <p> </p>    <p>来自:http://www.zcfy.cc/article/mastering-async-await-in-node-js-risingstack-4102.html</p>    <p> </p>