Async 函数 —— 让 promise 更友好

Rut1134 7年前
   <h2><strong>Async 函数--让 promise 更友好</strong></h2>    <p>Async 函数在 Chrome 55 中是默认开启的, 它确实非常不错。它允许你写出看起来像是同步的、基于 promise 的代码,但不会阻塞主线程。它让你的异步代码没那么“聪明”和更加可读。</p>    <p>Async 函数像这样工作:</p>    <pre>  <code class="language-javascript">async function myFirstAsyncFunction() {    try {      const fulfilledValue = await promise;    }    catch (rejectedValue) {      // …    }  }</code></pre>    <p>如果你在一个函数定义前使用 async 关键字,你就可以在函数内部使用 await 。当你 await (等待) 一个 promise,该函数会以非阻塞的方式中断,直到 promise 被解决。如果 promise 完成,你就可以得到结果。如果 promise 拒绝,就会抛出被拒绝的值。</p>    <h2><strong>示例:记录数据获取</strong></h2>    <p>假设我们想获取一个 URL 并记录响应的文本。这里是使用 promise 的方式:</p>    <pre>  <code class="language-javascript">function logFetch(url) {    return fetch(url)      .then(response => response.text())      .then(text => {        console.log(text);      }).catch(err => {        console.error('fetch failed', err);      });  }</code></pre>    <p>使用 async 函数做同样的事是这样的:</p>    <pre>  <code class="language-javascript">async function logFetch(url) {    try {      const response = await fetch(url);      console.log(await response.text());    }    catch (err) {      console.log('fetch failed', err);    }  }</code></pre>    <p>代码行数是一样的,但是所有的回调都不见了。这让它更容易阅读,特别是对那些不太熟悉 promise的人来说。</p>    <p>注意:你 await (等待)的所有东西都是通过 Promise.resolve() 传递的,因此你可以安全地 await (等待)非本地的 promise。</p>    <h2><strong>Async 返回值</strong></h2>    <p>Async 函数总是会返回一个 promise,不管你是否用了 await 。这个 promise 用 async 函数返回的任何值来解决,或者用 async 函数抛出的任何值来拒绝。因此给定如下代码:</p>    <pre>  <code class="language-javascript">// wait ms milliseconds  function wait(ms) {    return new Promise(r => setTimeout(r, ms));  }    async function hello() {    await wait(500);    return 'world';  }</code></pre>    <p>调用 hello() 会返回一个用 "world" 来完成的 promise。</p>    <pre>  <code class="language-javascript">async function foo() {    await wait(500);    throw Error('bar');  }</code></pre>    <p>调用 foo() 会返回一个用 Error('bar') 来拒绝的 promise。</p>    <h2><strong>示例:响应流</strong></h2>    <p>在更复杂的例子中 async 函数的好处更多。假设我们想在记录响应数据片段时将其变成数据流,并返回最终的大小。</p>    <p>注意:“记录片段” 这句话让我感到不适。</p>    <p>用 promise 是这样的:</p>    <pre>  <code class="language-javascript">function getResponseSize(url) {    return fetch(url).then(response => {      const reader = response.body.getReader();      let total = 0;        return reader.read().then(function processResult(result) {        if (result.done) return total;          const value = result.value;        total += value.length;        console.log('Received chunk', value);          return reader.read().then(processResult);      })    });  }</code></pre>    <p>看清楚了,我是 promise “地下党” Jake Archibald。看到我是怎样在它内部调用 processResult 并建立异步循环的了吗?这样写让我觉得自己“很聪明”。但是正如大多数“聪明的”代码一样,你不得不盯着它看很久才能搞清楚它在做什么,就像九十年代的那些魔眼照片一样。</p>    <p>让我们再用 async 函数来试试:</p>    <pre>  <code class="language-javascript">async function getResponseSize(url) {    const response = await fetch(url);    const reader = response.body.getReader();    let result = await reader.read();    let total = 0;      while (!result.done) {      const value = result.value;      total += value.length;      console.log('Received chunk', value);      // get the next result      result = await reader.read();    }      return total;  }</code></pre>    <p>所有的“小聪明”都不见了。让我自鸣得意的异步循环被一个可信任的、枯燥的 while 循环替代。好多了。将来,我们还有 async 迭代器 ,将会 用 for-of 循环替换 while 循环 ,这样就更好了。</p>    <p>注意:我有点喜欢数据流。</p>    <h2><strong>async 函数的其他语法</strong></h2>    <p>我们已经见过 async function() {} 了,但是 async 关键字还可以在其他的函数语法里使用:</p>    <h3>箭头函数</h3>    <pre>  <code class="language-javascript">// map some URLs to json-promises  const jsonPromises = urls.map(async url => {    const response = await fetch(url);    return response.json();  });</code></pre>    <p>注意: array.map(func) 并不在乎我给它传的是 async 函数,它只是把它当做一个返回 promise 的函数。它在调用第二个函数之前并不会等待第一个函数完成。</p>    <h3><strong>对象方法</strong></h3>    <pre>  <code class="language-javascript">const storage = {    async getAvatar(name) {      const cache = await caches.open('avatars');      return cache.match(`/avatars/${name}.jpg`);    }  };    storage.getAvatar('jaffathecake').then(…);</code></pre>    <h3><strong>类方法</strong></h3>    <pre>  <code class="language-javascript">class Storage {    constructor() {      this.cachePromise = caches.open('avatars');    }      async getAvatar(name) {      const cache = await this.cachePromise;      return cache.match(`/avatars/${name}.jpg`);    }  }    const storage = new Storage();  storage.getAvatar('jaffathecake').then(…);</code></pre>    <p>注意:类的构造函数和 getters/settings 不能是 async。</p>    <h2><strong>小心!避免过度强制先后顺序</strong></h2>    <p>尽管你写的代码看起来是同步的,要确保不要错过并行处理的机会。</p>    <pre>  <code class="language-javascript">async function series() {    await wait(500);    await wait(500);    return "done!";  }</code></pre>    <p>执行以上代码要 1000 毫秒,而:</p>    <pre>  <code class="language-javascript">async function parallel() {    const wait1 = wait(500);    const wait2 = wait(500);    await wait1;    await wait2;    return "done!";  }</code></pre>    <p>以上代码 500 毫秒完成,因为两个 wait 是同时发生的。让我们看看一个实际的例子。</p>    <h3><strong>例子:按顺序输出 fetch</strong></h3>    <p>假设我们想获取一系列的 URL 并尽快地按正确的顺序记录下来。</p>    <p><em>深呼吸</em> —— 用 promise 看起来是这样的:</p>    <pre>  <code class="language-javascript">function logInOrder(urls) {    // fetch all the URLs    const textPromises = urls.map(url => {      return fetch(url).then(response => response.text());    });      // log them in order    textPromises.reduce((chain, textPromise) => {      return chain.then(() => textPromise)        .then(text => console.log(text));    }, Promise.resolve());  }</code></pre>    <p>是的,没错,我在用 reduce 把一系列的 promise 串起来了。我“太聪明了”。但这是我们不应该有的“如此聪明的”代码。</p>    <p>然而,当我们把上面的代码转成 async 函数时,它变得 <em>过于强调先后顺序了</em> :</p>    <p>不推荐 —— 太强制先后顺序了</p>    <pre>  <code class="language-javascript">async function logInOrder(urls) {    for (const url of urls) {      const response = await fetch(url);      console.log(await response.text());    }  }</code></pre>    <p>看起来更简洁了,但是在第一个数据获取完全被读取前,第二个数据获取不会开始,后续的也是一样。这会比并发获取数据的 promise 例子慢得多。幸好还有一个理想的折中方案:</p>    <p>推荐 —— 很好,并行</p>    <pre>  <code class="language-javascript">async function logInOrder(urls) {    // fetch all the URLs in parallel    const textPromises = urls.map(async url => {      const response = await fetch(url);      return response.text();    });      // log them in sequence    for (const textPromise of textPromises) {      console.log(await textPromise);    }  }</code></pre>    <p>在这个例子中,URL 是并行获取和读取的,但是“聪明的” reduce 被一个标准的、枯燥的、可读性好的 for 循环取代了。</p>    <h2><strong>浏览器支持情况和变通方案</strong></h2>    <p>截止到本文写作时,Chrome 55 默认支持 async 函数,但所有主流浏览器都在开发中:</p>    <ul>     <li> <p>Edge - 版本 build 14342+ 加上 flag</p> </li>     <li> <p>Firefox - 活跃开发中</p> </li>     <li> <p>Safari - 活跃开发中</p> </li>    </ul>    <h3><strong>变通方案 - Generators</strong></h3>    <p>如果你的目标浏览器支持生成器,你可以模拟 async 函数。</p>    <p>Babel 可以帮你做到, 这里有个使用 Babel REPL 的例子 —— 注意看下转换后的代码多么相似。该转换是 Babel 的 es2017 预设版 。</p>    <p>注意:Babel REPL 发音很有趣。试试看。</p>    <p>我推荐使用转换的方式,因为一旦你的目标浏览器支持 async 函数了,你就可以关掉转换。不过如果你确实不想用转换器,你可以用 Babel 垫片 。不用这么写:</p>    <pre>  <code class="language-javascript">async function slowEcho(val) {    await wait(1000);    return val;  }</code></pre>    <p>你可以引入 这个垫片 并这样写:</p>    <pre>  <code class="language-javascript">const slowEcho = createAsyncFunction(function*(val) {    yield wait(1000);    return val;  });</code></pre>    <p>注意,你必须传递一个生成器 ( function* ) 到 createAsyncFunction ,并使用 yield 而不是 await 。除了这里,效果是一样的。</p>    <h3><strong>变通方案 —— 生成器转换</strong></h3>    <p>如果你需要支持较老的的浏览器,Babel 也可以转换生成器,允许你在低至 IE8 上使用 async 函数。为此你需要 Babel es2017 预设 以及 es2015 预设 。</p>    <p>输出结果没那么好看 ,所以小心代码膨胀。</p>    <h2><strong>Async 一切!</strong></h2>    <p>一旦所有浏览器都可以用 async 函数的时候,在所有返回 promise 的函数里使用它!它不仅让你的代码更整洁,而且能确保函数总是返回一个 promise。</p>    <p>我 早在2014年的时候 就对 async 函数非常兴奋了,看到它真正地落实到浏览器里,感觉非常棒。啊!</p>    <p>除非另外说明,本页的内容遵守 Creative Commons Attribution 3.0 License ,代码示例遵守 Apache 2.0 License 。</p>    <p> </p>    <p>来自:http://www.zcfy.cc/article/async-functions-making-promises-friendly-1566.html</p>    <p> </p>