Async 函数 —— 让 promise 更友好
Rut1134
8年前
<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>