diff options
Diffstat (limited to 'files/zh-cn/conflicting/learn/javascript')
4 files changed, 1685 insertions, 0 deletions
diff --git a/files/zh-cn/conflicting/learn/javascript/asynchronous/index.html b/files/zh-cn/conflicting/learn/javascript/asynchronous/index.html new file mode 100644 index 0000000000..278746ae22 --- /dev/null +++ b/files/zh-cn/conflicting/learn/javascript/asynchronous/index.html @@ -0,0 +1,524 @@ +--- +title: 选择正确的方法 +slug: conflicting/Learn/JavaScript/Asynchronous +translation_of: Learn/JavaScript/Asynchronous/Choosing_the_right_approach +original_slug: Learn/JavaScript/Asynchronous/Choosing_the_right_approach +--- +<div>{{LearnSidebar}}</div> + +<div>{{PreviousMenu("Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}</div> + +<p>为了完成这个模块,我们将简要讨论之前章节谈论过编码技术和功能,看看你应该使用哪一个,并提供适当的建议和提醒。随着时间的推移,我们可能会添加到此资源中。</p> + +<table class="learn-box standard-table"> + <tbody> + <tr> + <th scope="row">预备条件:</th> + <td>基本的计算机素养,对JavaScript基础知识的合理理解。</td> + </tr> + <tr> + <th scope="row">目标:</th> + <td>能够在使用不同的异步编程技术时做出合理的选择。</td> + </tr> + </tbody> +</table> + +<h2 id="异步回调">异步回调</h2> + +<p>通常在旧式API中找到,涉及将函数作为参数传递给另一个函数,然后在异步操作完成时调用该函数,以便回调可以依次对结果执行某些操作。这是promise的前身;它不那么高效或灵活。仅在必要时使用。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + </thead> + <tbody> + <tr> + <td>No</td> + <td>Yes (recursive callbacks)</td> + <td>Yes (nested callbacks)</td> + <td>No</td> + </tr> + </tbody> +</table> + +<h3 id="代码示例">代码示例</h3> + +<p>通过<a href="/en-US/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code> API</a>加载资源的示例(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/xhr-async-callback.html">run it live</a>,并查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/introducing/xhr-async-callback.html">see the source</a>):</p> + +<pre class="brush: js notranslate">function loadAsset(url, type, callback) { + let xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = type; + + xhr.onload = function() { + callback(xhr.response); + }; + + xhr.send(); +} + +function displayImage(blob) { + let objectURL = URL.createObjectURL(blob); + + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +} + +loadAsset('coffee.jpg', 'blob', displayImage);</pre> + +<h3 id="缺陷">缺陷</h3> + +<ul> + <li>嵌套回调可能很麻烦且难以阅读(即“回调地狱”)</li> + <li>每层嵌套都需要故障回调,而使用promises,您只需使用一个<code>.catch()</code>代码块来处理整个链的错误。</li> + <li>异步回调不是很优雅。</li> + <li>Promise回调总是按照它们放在事件队列中的严格顺序调用;异步回调不是。</li> + <li>当传入到一个第三方库时,异步回调对函数如何执行失去完全控制。</li> +</ul> + +<h3 id="浏览器兼容性">浏览器兼容性</h3> + +<p>非常好的一般支持,尽管API中回调的确切支持取决于特定的API。有关更具体的支持信息,请参阅您正在使用的API的参考文档。</p> + +<h3 id="更多信息">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">Introducing asynchronous JavaScript</a>, 尤其是 <a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing#Async_callbacks">Async callbacks</a></li> +</ul> + +<h2 id="setTimeout">setTimeout()</h2> + +<p><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code> 是一种允许您在经过任意时间后运行函数的方法</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>Yes</td> + <td>Yes (recursive timeouts)</td> + <td>Yes (nested timeouts)</td> + <td>No</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_2">代码示例</h3> + +<p>这里浏览器将在执行匿名函数之前等待两秒钟,然后将显示警报消息(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see it running live</a>,<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see the source code</a>):</p> + +<pre class="brush: js notranslate">let myGreeting = setTimeout(function() { + alert('Hello, Mr. Universe!'); +}, 2000)</pre> + +<h3 id="缺陷_2">缺陷</h3> + +<p>您可以使用递归的<code>setTimeout()</code>调用以类似于<code>setInterval()</code>的方式重复运行函数,使用如下代码:</p> + +<pre class="brush: js notranslate">let i = 1; +setTimeout(function run() { + console.log(i); + i++; + + setTimeout(run, 100); +}, 100);</pre> + +<p>递归<code>setTimeout()</code>和<code>setInterval()</code>之间存在差异:</p> + +<ul> + <li>递归<code>setTimeout()</code>保证两次执行间经过指定的时间量(在本例中为100ms);代码将运行,然后等待100毫秒再次运行。无论代码运行多长时间,间隔都是相同的。</li> + <li>使用<code>setInterval()</code>,我们选择的时间间隔包含了运行代码所花费的时间。(还是100ms为例)假设代码需要40毫秒才能运行 –– 间隔最终只会有60毫秒。</li> +</ul> + +<p>当你的代码有可能比你分配的时间间隔更长时间运行时,最好使用递归的<code>setTimeout()</code> ––这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。</p> + +<h3 id="浏览器兼容性_2">浏览器兼容性</h3> + +<p>{{Compat("api.WindowOrWorkerGlobalScope.setTimeout")}}</p> + +<h3 id="更多信息_2">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Cooperative asynchronous JavaScript: Timeouts and intervals</a>, in particular <a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals#setTimeout()">setTimeout()</a></li> + <li><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout() reference</a></li> +</ul> + +<h2 id="setInterval">setInterval()</h2> + +<p><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval()</a></code>函数允许重复执行一个函数,并设置时间间隔。不如<code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code>有效率,但允许您选择运行速率/帧速率。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>No</td> + <td>Yes</td> + <td>No (unless it is the same one)</td> + <td>No</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_3">代码示例</h3> + +<p>以下函数创建一个新的<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date">Date()</a></code>对象,使用<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString">toLocaleTimeString()</a></code>从中提取时间字符串,然后在UI中显示它。然后我们使用<code>setInterval()</code>每秒运行一次,创建每秒更新一次的数字时钟的效果(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see this live</a>,<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see the source</a>):</p> + +<pre class="brush: js notranslate">function displayTime() { + let date = new Date(); + let time = date.toLocaleTimeString(); + document.getElementById('demo').textContent = time; +} + +const createClock = setInterval(displayTime, 1000);</pre> + +<h3 id="缺陷_3">缺陷</h3> + +<ul> + <li>帧速率未针对运行动画的系统进行优化,并且可能效率低下。除非您需要选择特定(较慢)的帧速率,否则通常最好使用<code>requestAnimationFrame(</code>).</li> +</ul> + +<h3 id="浏览器兼容性_3">浏览器兼容性</h3> + +<p>{{Compat("api.WindowOrWorkerGlobalScope.setInterval")}}</p> + +<h3 id="更多信息_3">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Cooperative asynchronous JavaScript: Timeouts and intervals</a>, in particular <a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval()</a></li> + <li><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval() reference</a></li> +</ul> + +<h2 id="requestAnimationFrame">requestAnimationFrame()</h2> + +<p><code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code>是一种允许您以给定当前浏览器/系统的最佳帧速率重复且高效地运行函数的方法。除非您需要特定的速率帧,否则您应该尽可能使用它而不要去使用<code>setInterval()/recursive setTimeout()</code>。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>No</td> + <td>Yes</td> + <td>No (unless it is the same one)</td> + <td>No</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_4">代码示例</h3> + +<p>一个简单的动画旋转器;你可以查看<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">example live on GitHub</a>(参见 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">source code</a> ):</p> + +<pre class="brush: js notranslate">const spinner = document.querySelector('div'); +let rotateCount = 0; +let startTime = null; +let rAF; + +function draw(timestamp) { + if(!startTime) { + startTime = timestamp; + } + + let rotateCount = (timestamp - startTime) / 3; + + spinner.style.transform = 'rotate(' + rotateCount + 'deg)'; + + if(rotateCount > 359) { + rotateCount = 0; + } + + rAF = requestAnimationFrame(draw); +} + +draw();</pre> + +<h3 id="缺陷_4">缺陷</h3> + +<ul> + <li>您无法使用<code>requestAnimationFrame()</code>选择特定的帧速率。如果需要以较慢的帧速率运行动画,则需要使用<code>setInterval()</code>或递归的<code>setTimeout()</code>。</li> +</ul> + +<h3 id="浏览器兼容性_4">浏览器兼容性</h3> + +<p>{{Compat("api.Window.requestAnimationFrame")}}</p> + +<h3 id="更多信息_4">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Cooperative asynchronous JavaScript: Timeouts and intervals</a>, in particular <a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals#requestAnimationFrame()">requestAnimationFrame()</a></li> + <li><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame() reference</a></li> +</ul> + +<h2 id="Promises">Promises</h2> + +<p><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a> 是一种JavaScript功能,允许您运行异步操作并等到它完全完成后再根据其结果运行另一个操作。 Promise是现代异步JavaScript的支柱。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>No</td> + <td>No</td> + <td>Yes</td> + <td>See <code>Promise.all()</code>, below</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_5">代码示例</h3> + +<p>以下代码从服务器获取图像并将其显示在 {{htmlelement("img")}} 元素中;(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/simple-fetch-chained.html">see it live also</a>,<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/simple-fetch-chained.html">the source code</a>):</p> + +<pre class="brush: js notranslate">fetch('coffee.jpg') +.then(response => response.blob()) +.then(myBlob => { + let objectURL = URL.createObjectURL(myBlob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +}) +.catch(e => { + console.log('There has been a problem with your fetch operation: ' + e.message); +});</pre> + +<h3 id="缺陷_5">缺陷</h3> + +<p>Promise链可能很复杂,难以解析。如果你嵌套了许多promises,你最终可能会遇到类似的麻烦来回调地狱。例如:</p> + +<pre class="brush: js notranslate">remotedb.allDocs({ + include_docs: true, + attachments: true +}).then(function (result) { + var docs = result.rows; + docs.forEach(function(element) { + localdb.put(element.doc).then(function(response) { + alert("Pulled doc with id " + element.doc._id + " and added to local db."); + }).catch(function (err) { + if (err.name == 'conflict') { + localdb.get(element.doc._id).then(function (resp) { + localdb.remove(resp._id, resp._rev).then(function (resp) { +// et cetera...</pre> + +<p>最好使用promises的链功能,这样使用更平顺,更易于解析的结构:</p> + +<pre class="brush: js notranslate">remotedb.allDocs(...).then(function (resultOfAllDocs) { + return localdb.put(...); +}).then(function (resultOfPut) { + return localdb.get(...); +}).then(function (resultOfGet) { + return localdb.put(...); +}).catch(function (err) { + console.log(err); +});</pre> + +<p>乃至:</p> + +<pre class="brush: js notranslate">remotedb.allDocs(...) +.then(resultOfAllDocs => { + return localdb.put(...); +}) +.then(resultOfPut => { + return localdb.get(...); +}) +.then(resultOfGet => { + return localdb.put(...); +}) +.catch(err => console.log(err));</pre> + +<p>这涵盖了很多基础知识。对于更完整的论述,请参阅诺兰劳森的<a href="https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html">We have a problem with promises</a>。</p> + +<h3 id="浏览器兼容性_5">浏览器兼容性</h3> + +<p>{{Compat("javascript.builtins.Promise")}}</p> + +<h3 id="更多信息_5">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises">Graceful asynchronous programming with Promises</a></li> + <li><a href="/en-US/docs/Web/JavaScript/Guide/Using_promises">Using promises</a></li> + <li><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise reference</a></li> +</ul> + +<h2 id="Promise.all">Promise.all()</h2> + +<p>一种JavaScript功能,允许您等待多个promises完成,然后根据所有其他promises的结果运行进一步的操作。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>No</td> + <td>No</td> + <td>No</td> + <td>Yes</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_6">代码示例</h3> + +<p>以下示例从服务器获取多个资源,并使用<code>Promise.all()</code>等待所有资源可用,然后显示所有这些资源–– <a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/promise-all.html">see it live</a>,并查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/promise-all.html">source code</a>:</p> + +<pre class="brush: js notranslate">function fetchAndDecode(url, type) { + // Returning the top level promise, so the result of the entire chain is returned out of the function + return fetch(url).then(response => { + // Depending on what type of file is being fetched, use the relevant function to decode its contents + if(type === 'blob') { + return response.blob(); + } else if(type === 'text') { + return response.text(); + } + }) + .catch(e => { + console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); + }); +} + +// Call the fetchAndDecode() method to fetch the images and the text, and store their promises in variables +let coffee = fetchAndDecode('coffee.jpg', 'blob'); +let tea = fetchAndDecode('tea.jpg', 'blob'); +let description = fetchAndDecode('description.txt', 'text'); + +// Use Promise.all() to run code only when all three function calls have resolved +Promise.all([coffee, tea, description]).then(values => { + console.log(values); + // Store each value returned from the promises in separate variables; create object URLs from the blobs + let objectURL1 = URL.createObjectURL(values[0]); + let objectURL2 = URL.createObjectURL(values[1]); + let descText = values[2]; + + // Display the images in <img> elements + let image1 = document.createElement('img'); + let image2 = document.createElement('img'); + image1.src = objectURL1; + image2.src = objectURL2; + document.body.appendChild(image1); + document.body.appendChild(image2); + + // Display the text in a paragraph + let para = document.createElement('p'); + para.textContent = descText; + document.body.appendChild(para); +});</pre> + +<h3 id="缺陷_6">缺陷</h3> + +<ul> + <li>如果<code>Promise.all()</code>拒绝,那么你在其数组参数中输入的一个或多个promise(s)就会被拒绝,或者可能根本不返回promises。你需要检查每一个,看看他们返回了什么。</li> +</ul> + +<h3 id="浏览器兼容性_6">浏览器兼容性</h3> + +<p>{{Compat("javascript.builtins.Promise.all")}}</p> + +<h3 id="更多信息_6">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises#Running_code_in_response_to_multiple_promises_fulfilling">Running code in response to multiple promises fulfilling</a></li> + <li><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all() reference</a></li> +</ul> + +<h2 id="Asyncawait">Async/await</h2> + +<p>构造在promises之上的语法糖,允许您使用更像编写同步回调代码的语法来运行异步操作。</p> + +<table class="standard-table"> + <caption>Useful for...</caption> + <thead> + <tr> + <th scope="col">Single delayed operation</th> + <th scope="col">Repeating operation</th> + <th scope="col">Multiple sequential operations</th> + <th scope="col">Multiple simultaneous operations</th> + </tr> + <tr> + <td>No</td> + <td>No</td> + <td>Yes</td> + <td>Yes (in combination with <code>Promise.all()</code>)</td> + </tr> + </thead> +</table> + +<h3 id="代码示例_7">代码示例</h3> + +<p>以下示例是我们之前看到的简单承诺示例的重构,该示例获取并显示图像,使用async / await编写(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/simple-refactored-fetch.html">see it live</a>,并查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/simple-refactored-fetch.html">source code</a>):</p> + +<pre class="brush: js notranslate">async function myFetch() { + let response = await fetch('coffee.jpg'); + let myBlob = await response.blob(); + + let objectURL = URL.createObjectURL(myBlob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +} + +myFetch();</pre> + +<h3 id="缺陷_7">缺陷</h3> + +<ul> + <li>您不能在非<code>async</code>函数内或代码的顶级上下文环境中使用<code>await</code>运算符。这有时会导致需要创建额外的函数封包,这在某些情况下会略微令人沮丧。但大部分时间都值得。</li> + <li>浏览器对async / await的支持不如promises那样好。如果你想使用async / await但是担心旧的浏览器支持,你可以考虑使用<a href="https://babeljs.io/">BabelJS</a> 库 - 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。</li> +</ul> + +<h3 id="浏览器兼容性_7">浏览器兼容性</h3> + +<p>{{Compat("javascript.statements.async_function")}}</p> + +<h3 id="更多信息_7">更多信息</h3> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Async_await">Making asynchronous programming easier with async and await</a></li> + <li><a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function">Async function reference</a></li> + <li><a href="/en-US/docs/Web/JavaScript/Reference/Operators/await">Await operator reference</a></li> +</ul> + +<p>{{PreviousMenu("Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}</p> + +<h2 id="本章内容">本章内容</h2> + +<ul> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Concepts">异步编程的基本概念</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Introducing">JavaScript异步简介</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">JavaScript合作异步:超时和间隔</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises">使用Promises:优雅的异步编程</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await">使用async和await:让异步编程更简单</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">选择正确的方法</a></li> +</ul> diff --git a/files/zh-cn/conflicting/learn/javascript/asynchronous/introducing/index.html b/files/zh-cn/conflicting/learn/javascript/asynchronous/introducing/index.html new file mode 100644 index 0000000000..291cf15837 --- /dev/null +++ b/files/zh-cn/conflicting/learn/javascript/asynchronous/introducing/index.html @@ -0,0 +1,163 @@ +--- +title: 通用异步编程概念 +slug: conflicting/Learn/JavaScript/Asynchronous/Introducing +tags: + - JavaScript + - Promises + - Threads + - 学习 + - 异步 + - 阻塞 +translation_of: Learn/JavaScript/Asynchronous/Concepts +original_slug: Learn/JavaScript/Asynchronous/Concepts +--- +<div>{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}</div> + +<p>在本文中,我们将介绍与异步编程相关的一些重要概念,以及它们在浏览器和JavaScript里的体现。在学习本系列的其他文章之前,你应该先理解这些概念。</p> + +<table class="learn-box standard-table"> + <tbody> + <tr> + <th scope="row">预备条件:</th> + <td>拥有基本的计算机知识,对JavaScript原理有一定了解。</td> + </tr> + <tr> + <th scope="row">目标:</th> + <td>理解异步编程的基本概念,以及异步编程在浏览器和JavaScript里面的表现。</td> + </tr> + </tbody> +</table> + +<h2 id="异步">异步?</h2> + +<p>通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕.</p> + +<p>Mac 用户有时会经历过这种旋转的彩虹光标(常称为沙滩球),操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。</p> + +<p><img alt="multi-colored macos beachball busy spinner" src="https://mdn.mozillademos.org/files/16577/beachball.jpg" style="display: block; margin: 0 auto;"></p> + +<p>这是令人沮丧的体验,没有充分利用计算机的计算能力 — 尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,同时计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是<strong>异步编程</strong>的出发点。你正在使用的编程环境(就web开发而言,编程环境就是web浏览器)负责为你提供异步运行此类任务的API。</p> + +<h2 id="产生阻塞的代码">产生阻塞的代码</h2> + +<p>异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做<strong>阻塞;</strong>这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。</p> + +<p>我们来看一些<strong>阻塞</strong>的例子。</p> + +<p>例子: <a href="https://github.com/mdn/learning-area/tree/master/javascript/asynchronous/introducing">simple-sync.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync.html">see it running live</a>), 在按钮上添加了一个事件监听器,当按钮被点击,它就开始运行一个非常耗时的任务(计算1千万个日期,并在console里显示最后一个日期),然后在DOM里面添加一个段落:</p> + +<pre class="brush: js">const btn = document.querySelector('button'); +btn.addEventListener('click', () => { + let myDate; + for(let i = 0; i < 10000000; i++) { + let date = new Date(); + myDate = date + } + + console.log(myDate); + + let pElem = document.createElement('p'); + pElem.textContent = 'This is a newly-added paragraph.'; + document.body.appendChild(pElem); +});</pre> + +<p>运行这个例子的时候,打开JavaScript console,然后点击按钮 — 你会注意到,直到日期的运算结束,最后一个日期在console上显示出来,段落才会出现在网页上。代码按照源代码的顺序执行,只有前面的代码结束运行,后面的代码才会执行。</p> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: 这个例子不现实:在实际情况中一般不会发生,没有谁会计算1千万次日期,它仅仅提供一个非常直观的体验.</p> +</div> + +<p>第二个例子, <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/introducing/simple-sync-ui-blocking.html">simple-sync-ui-blocking.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-ui-blocking.html">see it live</a>), 我们模拟一个在现实的网页可能遇到的情况:因为渲染UI而阻塞用户的互动,这个例子有2个按钮:</p> + +<ul> + <li>"Fill canvas" : 点击的时候用1百万个蓝色的圆填满整个{{htmlelement("canvas")}} .</li> + <li>"Click me for alert" :点击显示alert 消息.</li> +</ul> + +<pre class="brush: js">function expensiveOperation() { + for(let i = 0; i < 1000000; i++) { + ctx.fillStyle = 'rgba(0,0,255, 0.2)'; + ctx.beginPath(); + ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false); + ctx.fill() + } +} + +fillBtn.addEventListener('click', expensiveOperation); + +alertBtn.addEventListener('click', () => + alert('You clicked me!') +);</pre> + +<p>如果你点击第一个按钮,然后快速点击第二个,会注意到alert消息并没有出现,只有等到圆圈都画完以后,才会出现:因为第一个操作没有完成之前阻塞了第二个操作的运行.</p> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: 当然,这个例子也很丑陋,因为我们只是在模拟阻塞效果。但在现实中,这是一个很常见的问题。开发人员们一直在努力缓解它。</p> +</div> + +<p>为什么是这样? 答案是:JavaScript一般来说是单线程的(<strong>single threaded</strong>)<strong>。</strong>接着我们来介绍<strong>线程</strong>的概念。</p> + +<h2 id="线程">线程</h2> + +<p>一个<strong>线程</strong>是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:</p> + +<pre>Task A --> Task B --> Task C</pre> + +<p>每个任务顺序执行,只有前面的结束了,后面的才能开始。</p> + +<p>正如我们之前所说,现在的计算机大都有多个内核(core),因此可以同时执行多个任务。支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:</p> + +<pre>Thread 1: Task A --> Task B +Thread 2: Task C --> Task D</pre> + +<h3 id="JavaScript_是单线程的">JavaScript 是单线程的</h3> + +<p>JavaScript 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(<strong>main thread</strong>)。我们上面的例子运行如下:</p> + +<pre>Main thread: Render circles to canvas --> Display alert()</pre> + +<p>经过一段时间,JavaScript获得了一些工具来帮助解决这种问题。通过 <a href="/en-US/docs/Web/API/Web_Workers_API">Web workers</a> 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)</p> + +<pre>Main thread: Task A --> Task C +Worker thread: Expensive task B</pre> + +<p>记住这些,请查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/introducing/simple-sync-worker.html">simple-sync-worker.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-worker.html">see it running live</a>) , 再次打开浏览器的JavaScript 控制台。这个例子重写了前例:在一个单独的worker线程中计算一千万次日期,你再点击按钮,现在浏览器可以在日期计算完成之前显示段落,阻塞消失了。</p> + +<h2 id="异步代码">异步代码</h2> + +<p>web workers相当有用,但是他们确实也有局限。主要的一个问题是他们不能访问 {{Glossary("DOM")}} — 不能让一个worker直接更新UI。我们不能在worker里面渲染1百万个蓝色圆圈,它基本上只能做算数的苦活。</p> + +<p>其次,虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。考虑下面的情况:</p> + +<pre>Main thread: Task A --> Task B</pre> + +<p>在这种情况下,比如说Task A 正在从服务器上获取一个图片之类的资源,Task B 准备在图片上加一个滤镜。如果开始运行Task A 后立即尝试运行Task B,你将会得到一个错误,因为图像还没有获取到。</p> + +<pre>Main thread: Task A --> Task B --> |Task D| +Worker thread: Task C -----------> | |</pre> + +<p>在这种情况下,假设Task D 要同时使用 Task B 和Task C的结果,如果我们能保证这两个结果同时提供,程序可能正常运行,但是这不太可能。如果Task D 尝试在其中一个结果尚未可用的情况下就运行,程序就会抛出一个错误。</p> + +<p>为了解决这些问题,浏览器允许我们异步运行某些操作。像<a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a> 这样的功能就允许让一些操作运行 (比如:从服务器上获取图片),然后等待直到结果返回,再运行其他的操作:</p> + +<pre>Main thread: Task A Task B + Promise: |__async operation__|</pre> + +<p>由于操作发生在其他地方,因此在处理异步操作的时候,主线程不会被阻塞。</p> + +<p><span class="tlid-translation translation" lang="zh-CN"><span title="">我们将在下一篇文章中开始研究如何编写异步代码。</span> <span title="">非常令人兴奋,对吧?</span> <span title="">继续阅读!</span></span></p> + +<h2 id="总结">总结</h2> + +<p>围绕异步编程领域,现代软件设计正在加速旋转,就为了让程序在一个时间内做更多的事情。当你使用更新更强大的API时,你会发现在更多的情况下,使用异步编程是唯一的途径。以前写异步代码很困难,现在也需要你来适应,但是已经变容易了很多。在余下的部分,我们将进一步探讨异步代码的重要性,以及如何设计代码来防止前面已经提到过的问题。</p> + +<h2 id="模块大纲">模块大纲</h2> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Concepts">通用异步编程概念</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">介绍异步JavaScript</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">合作异步JavaScript: Timeouts and intervals</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises">使用Promises优雅的异步编程</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Async_await">async and await:让异步编程更容易</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">选择正确的方法</a></li> +</ul> diff --git a/files/zh-cn/conflicting/learn/javascript/asynchronous/promises/index.html b/files/zh-cn/conflicting/learn/javascript/asynchronous/promises/index.html new file mode 100644 index 0000000000..3c73fecd3d --- /dev/null +++ b/files/zh-cn/conflicting/learn/javascript/asynchronous/promises/index.html @@ -0,0 +1,380 @@ +--- +title: async和await:让异步编程更简单 +slug: conflicting/Learn/JavaScript/Asynchronous/Promises +translation_of: Learn/JavaScript/Asynchronous/Async_await +original_slug: Learn/JavaScript/Asynchronous/Async_await +--- +<div>{{LearnSidebar}}</div> + +<div>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}</div> + +<p class="summary"><a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async functions</a> 和 <code><a href="/en-US/docs/Web/JavaScript/Reference/Operators/await">await</a></code> 关键字是最近添加到JavaScript语言里面的。它们是ECMAScript 2017 JavaScript版的一部分(参见<a href="/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_Next_support_in_Mozilla">ECMAScript Next support in Mozilla</a>)。简单来说,它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。本文为您提供了您需要了解的内容。</p> + +<table class="learn-box standard-table"> + <tbody> + <tr> + <th scope="row">先决条件:</th> + <td>基本的计算机知识,较好理解 JavaScript 基础,以及理解一般异步代码和 promises 。</td> + </tr> + <tr> + <th scope="row">目标:</th> + <td>理解并使用 promise</td> + </tr> + </tbody> +</table> + +<h2 id="asyncawait_基础">async/await 基础</h2> + +<p>在代码中使用 async / await 有两个部分。</p> + +<h3 id="async_关键字">async 关键字</h3> + +<p>首先,我们使用 <code>async</code> 关键字,把它放在函数声明之前,使其成为 <a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async function</a>。异步函数是一个知道怎样使用 <code>await</code> 关键字调用异步代码的函数。</p> + +<p>尝试在浏览器的JS控制台中键入以下行:</p> + +<pre class="brush: js notranslate">function hello() { return "Hello" }; +hello();</pre> + +<p>该函数返回“Hello” —— 没什么特别的,对吧?</p> + +<p>如果我们将其变成异步函数呢?请尝试以下方法:</p> + +<pre class="brush: js notranslate">async function hello() { return "Hello" }; +hello();</pre> + +<p>哈。现在调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。</p> + +<p>你也可以创建一个异步函数表达式(参见 <a href="/en-US/docs/Web/JavaScript/Reference/Operators/async_function">async function expression</a> ),如下所示:</p> + +<pre class="brush: js notranslate">let hello = async function() { return "Hello" }; +hello();</pre> + +<p>你可以使用箭头函数:</p> + +<pre class="brush: js notranslate">let hello = async () => { return "Hello" };</pre> + +<p>这些都基本上是一样的。</p> + +<p>要实际使用promise完成时返回的值,我们可以使用<code>.then()</code>块,因为它返回的是 promise:</p> + +<pre class="brush: js notranslate">hello().then((value) => console.log(value))</pre> + +<p>甚至只是简写如</p> + +<pre class="brush: js notranslate">hello().then(console.log)</pre> + +<p>这就像我们在上一篇文章中看到的那样。</p> + +<p>将 <code>async</code> 关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用 <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font> 带来的任何潜在开销。在函数声明为 <code>async</code> 时,JavaScript引擎会添加必要的处理,以优化你的程序。爽!</p> + +<h3 id="await关键字">await关键字</h3> + +<p>当 <a href="/en-US/docs/Web/JavaScript/Reference/Operators/await">await</a> 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, <strong><font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font> 只在异步函数里面才起作用</strong>。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。</p> + +<p>您可以在调用任何返回Promise的函数时使用 <strong><font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font></strong>,包括Web API函数。</p> + +<p>这是一个简单的示例:</p> + +<pre class="brush: js notranslate">async function hello() { + return greeting = await Promise.resolve("Hello"); +}; + +hello().then(alert);</pre> + +<p>当然,上面的示例不是很有用,但它确实展示了语法。让我们继续,看一个真实示例。</p> + +<h2 id="使用_asyncawait_重写_promise_代码"> 使用 async/await 重写 promise 代码</h2> + +<p>让我们回顾一下我们在上一篇文章中简单的 fetch 示例:</p> + +<pre class="brush: js notranslate">fetch('coffee.jpg') +.then(response => response.blob()) +.then(myBlob => { + let objectURL = URL.createObjectURL(myBlob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +}) +.catch(e => { + console.log('There has been a problem with your fetch operation: ' + e.message); +});</pre> + +<p>到现在为止,你应该对 promises 及其工作方式有一个较好的理解。让我们将其转换为使用async / await看看它使事情变得简单了多少:</p> + +<pre class="brush: js notranslate">async function myFetch() { + let response = await fetch('coffee.jpg'); + let myBlob = await response.blob(); + + let objectURL = URL.createObjectURL(myBlob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +} + +myFetch() +.catch(e => { + console.log('There has been a problem with your fetch operation: ' + e.message); +});</pre> + +<p>它使代码简单多了,更容易理解 —— 去除了到处都是 <code>.then()</code> 代码块!</p> + +<p>由于 <code>async</code> 关键字将函数转换为 promise,您可以重构以上代码 —— 使用 promise 和 await 的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:</p> + +<pre class="brush: js notranslate">async function myFetch() { + let response = await fetch('coffee.jpg'); + return await response.blob(); +} + +myFetch().then((blob) => { + let objectURL = URL.createObjectURL(blob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +});</pre> + +<p>您可以尝试自己输入示例,或运行我们的 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/simple-fetch-async-await.html">live example</a> (另请参阅<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/simple-fetch-async-await.html">source code</a>)。</p> + +<h3 id="它到底是如何工作的?">它到底是如何工作的?</h3> + +<p>您会注意到我们已经将代码封装在函数中,并且我们在 <code>function</code> 关键字之前包含了 <code>async</code> 关键字。这是必要的 –– 您必须创建一个异步函数来定义一个代码块,在其中运行异步代码; <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font> 只能在异步函数内部工作。</p> + +<p>在<code>myFetch()</code>函数定义中,您可以看到代码与先前的 promise 版本非常相似,但存在一些差异。不需要附加 <code>.then()</code> 代码块到每个promise-based方法的结尾,你只需要在方法调用前添加 <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font> 关键字,然后把结果赋给变量。<font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.5);">await</span></font> 关键字使JavaScript运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,您的代码将继续从下一行开始执行。例如:</p> + +<pre class="brush: js notranslate">let response = await fetch('coffee.jpg');</pre> + +<p>解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 <code>fetch()</code> 返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 <code>response</code> 变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个<code><a href="/en-US/docs/Web/API/Blob">Blob</a></code>。Blob这行也调用基于异步promise的方法,因此我们也在此处使用<code>await</code>。当操作结果返回时,我们将它从<code>myFetch()</code>函数中返回。</p> + +<p>这意味着当我们调用<code>myFetch()</code>函数时,它会返回一个promise,因此我们可以将<code>.then()</code>链接到它的末尾,在其中我们处理显示在屏幕上的<code>blob</code>。</p> + +<p>你可能已经觉得“这真的很酷!”,你是对的 —— 用更少的.<code>then()</code>块来封装代码,同时它看起来很像同步代码,所以它非常直观。</p> + +<h3 id="添加错误处理">添加错误处理</h3> + +<p>如果你想添加错误处理,你有几个选择。</p> + +<p>您可以将同步的 <code><a href="/en-US/docs/Web/JavaScript/Reference/Statements/try...catch">try...catch</a></code> 结构和 <code>async/await</code> 一起使用 。此示例扩展了我们上面展示的第一个版本代码:</p> + +<pre class="brush: js notranslate">async function myFetch() { + try { + let response = await fetch('coffee.jpg'); + let myBlob = await response.blob(); + + let objectURL = URL.createObjectURL(myBlob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); + } catch(e) { + console.log(e); + } +} + +myFetch();</pre> + +<p><code>catch() {}</code> 代码块会接收一个错误对象 <code>e</code> ; 我们现在可以将其记录到控制台,它将向我们提供详细的错误消息,显示错误被抛出的代码中的位置。</p> + +<p>如果你想使用我们上面展示的第二个(重构)代码版本,你最好继续混合方式并将 <code>.catch()</code> 块链接到 <code>.then()</code> 调用的末尾,就像这样:</p> + +<pre class="brush: js notranslate">async function myFetch() { + let response = await fetch('coffee.jpg'); + return await response.blob(); +} + +myFetch().then((blob) => { + let objectURL = URL.createObjectURL(blob); + let image = document.createElement('img'); + image.src = objectURL; + document.body.appendChild(image); +}) +.catch((e) => + console.log(e) +);</pre> + +<p>这是因为 <code>.catch()</code> 块将捕获来自异步函数调用和promise链中的错误。如果您在此处使用了<code>try/catch</code> 代码块,则在调用 <code>myFetch()</code> 函数时,您仍可能会收到未处理的错误。</p> + +<p>您可以在GitHub上找到这两个示例:</p> + +<ul> + <li><a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/simple-fetch-async-await-try-catch.html">simple-fetch-async-await-try-catch.html</a> (参见 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/simple-fetch-async-await-try-catch.html">源码</a>)</li> + <li><a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/simple-fetch-async-await-promise-catch.html">simple-fetch-async-await-promise-catch.html</a> (参见 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/simple-fetch-async-await-promise-catch.html">源码</a>)</li> +</ul> + +<h2 id="等待Promise.all">等待Promise.all()</h2> + +<p><code>async / await</code> 建立在 <a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promises</a> 之上,因此它与promises提供的所有功能兼容。这包括<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all()</a></code> –– 你完全可以通过调用 <code>await</code> <code>Promise.all()</code> 将所有结果返回到变量中,就像同步代码一样。让我们再次回到<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/promise-all.html">上一篇中看到的例子</a>。在单独的选项卡中打开它,以便您可以与下面显示的新版本进行比较和对比。</p> + +<p>将其转换为 async / await(请参阅 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/promise-all-async-await.html">样例</a> 和 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/promise-all-async-await.html">源码</a>),现在看起来像这样:</p> + +<pre class="brush: js notranslate">async function fetchAndDecode(url, type) { + let response = await fetch(url); + + let content; + + if(type === 'blob') { + content = await response.blob(); + } else if(type === 'text') { + content = await response.text(); + } + + return content; +} + +async function displayContent() { + let coffee = fetchAndDecode('coffee.jpg', 'blob'); + let tea = fetchAndDecode('tea.jpg', 'blob'); + let description = fetchAndDecode('description.txt', 'text'); + + let values = await Promise.all([coffee, tea, description]); + + let objectURL1 = URL.createObjectURL(values[0]); + let objectURL2 = URL.createObjectURL(values[1]); + let descText = values[2]; + + let image1 = document.createElement('img'); + let image2 = document.createElement('img'); + image1.src = objectURL1; + image2.src = objectURL2; + document.body.appendChild(image1); + document.body.appendChild(image2); + + let para = document.createElement('p'); + para.textContent = descText; + document.body.appendChild(para); +} + +displayContent() +.catch((e) => + console.log(e) +);</pre> + +<p>可以看到 <code>fetchAndDecode()</code> 函数只进行了一丁点的修改就转换成了异步函数。请看<code>Promise.all()</code> 行:</p> + +<pre class="brush: js notranslate">let values = await Promise.all([coffee, tea, description]);</pre> + +<p>在这里,通过使用<code>await</code>,我们能够在三个promise的结果都可用的时候,放入<code>values</code>数组中。这看起来非常像同步代码。我们需要将所有代码封装在一个新的异步函数<code>displayContent()</code> 中,尽管没有减少很多代码,但能够将大部分代码从 <code>.then()</code> 代码块移出,使代码得到了简化,更易读。</p> + +<p>为了错误处理,我们在 <code>displayContent()</code> 调用中包含了一个 <code>.catch()</code> 代码块;这将处理两个函数中出现的错误。</p> + +<div class="blockIndicator note"> +<p><strong>注意</strong>: 也可以在异步函数中使用同步 <code><a href="/en-US/docs/Web/JavaScript/Reference/Statements/try...catch#The_finally_clause">finally</a></code> 代码块代替 <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally">.finally()</a></code> 异步代码块,以显示操作如何进行的最终报告——您可以在我们的 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/promise-finally-async-await.html">live example</a> (查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/promise-finally-async-await.html">源代码</a>)中看到这一点。</p> +</div> + +<h2 id="asyncawait的缺陷">async/await的缺陷</h2> + +<p>了解<code>Async/await</code>是非常有用的,但还有一些缺点需要考虑。</p> + +<p><code>Async/await</code> 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 <code>await</code> 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。</p> + +<p>这意味着您的代码可能会因为大量<code>await</code>的promises相继发生而变慢。每个<code>await</code>都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用<code>async/await</code>时那样)。</p> + +<p>有一种模式可以缓解这个问题——通过将 <code>Promise</code> 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。</p> + +<p>我们有两个可用的例子 —— <a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/slow-async-await.html">slow-async-await.html</a>(参见<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/slow-async-await.html">source code</a>)和<a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/fast-async-await.html">fast-async-await.html</a>(参见<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/async-await/fast-async-await.html">source code</a>)。它们都以自定义promise函数开始,该函数使用<code>setTimeout()</code> 调用伪造异步进程:</p> + +<pre class="brush: js notranslate">function timeoutPromise(interval) { + return new Promise((resolve, reject) => { + setTimeout(function(){ + resolve("done"); + }, interval); + }); +};</pre> + +<p>然后每个包含一个 <code>timeTest()</code> 异步函数,等待三个 <code>timeoutPromise()</code> 调用:</p> + +<pre class="brush: js notranslate">async function timeTest() { + ... +}</pre> + +<p>每一个都以记录开始时间结束,查看 <code>timeTest()</code> promise 需要多长时间才能完成,然后记录结束时间并报告操作总共需要多长时间:</p> + +<pre class="brush: js notranslate">let startTime = Date.now(); +timeTest().then(() => { + let finishTime = Date.now(); + let timeTaken = finishTime - startTime; + alert("Time taken in milliseconds: " + timeTaken); +})</pre> + +<p><code>timeTest()</code> 函数在每种情况下都不同。</p> + +<p>在<a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/slow-async-await.html">slow-async-await.html</a>示例中,<code>timeTest()</code> 如下所示:</p> + +<pre class="brush: js notranslate">async function timeTest() { + await timeoutPromise(3000); + await timeoutPromise(3000); + await timeoutPromise(3000); +}</pre> + +<p>在这里,我们直接等待所有三个timeoutPromise()调用,使每个调用3秒钟。后续的每一个都被迫等到最后一个完成 - 如果你运行第一个例子,你会看到弹出框报告的总运行时间大约为9秒。</p> + +<p>在<a href="https://mdn.github.io/learning-area/javascript/asynchronous/async-await/fast-async-await.html">fast-async-await.html</a>示例中,<code>timeTest()</code> 如下所示:</p> + +<pre class="brush: js notranslate">async function timeTest() { + const timeoutPromise1 = timeoutPromise(3000); + const timeoutPromise2 = timeoutPromise(3000); + const timeoutPromise3 = timeoutPromise(3000); + + await timeoutPromise1; + await timeoutPromise2; + await timeoutPromise3; +}</pre> + +<p>在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。</p> + +<p>接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!</p> + +<p>您必须仔细测试您的代码,并在性能开始受损时牢记这一点。</p> + +<p>另一个小小的不便是你必须将等待执行的promise封装在异步函数中。</p> + +<h2 id="Asyncawait_的类方法">Async/await 的类方法</h2> + +<p>最后值得一提的是,我们可以在类/对象方法前面添加<code>async</code>,以使它们返回promises,并<code>await</code>它们内部的promises。查看 <a href="/en-US/docs/Learn/JavaScript/Objects/Inheritance#ECMAScript_2015_Classes">ES class code we saw in our object-oriented JavaScript article</a>,然后查看使用异步方法的修改版本:</p> + +<pre class="brush: js notranslate">class Person { + constructor(first, last, age, gender, interests) { + this.name = { + first, + last + }; + this.age = age; + this.gender = gender; + this.interests = interests; + } + + async greeting() { + return await Promise.resolve(`Hi! I'm ${this.name.first}`); + }; + + farewell() { + console.log(`${this.name.first} has left the building. Bye for now!`); + }; +} + +let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);</pre> + +<p>第一个实例方法可以使用如下:</p> + +<pre class="brush: js notranslate">han.greeting().then(console.log);</pre> + +<h2 id="浏览器的支持">浏览器的支持</h2> + +<p>决定是否使用 async/await 时的一个考虑因素是支持旧浏览器。它们适用于大多数浏览器的现代版本,与promise相同; 主要的支持问题存在于Internet Explorer和Opera Mini。</p> + +<p>如果你想使用async/await但是担心旧的浏览器支持,你可以考虑使用<a href="https://babeljs.io/">BabelJS</a>库 —— 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。在遇到不支持async/await 的浏览器时,Babel的 polyfill 可以自动提供适用于旧版浏览器的实现。</p> + +<h2 id="总结">总结</h2> + +<p>async/await提供了一种很好的,简化的方法来编写更易于阅读和维护的异步代码。即使浏览器支持在撰写本文时比其他异步代码机制更受限制,但无论是现在还是将来,都值得学习和考虑使用。</p> + +<p>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}</p> + +<h2 id="本章内容">本章内容</h2> + +<ul> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Concepts">异步编程的基本概念</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Introducing">JavaScript异步简介</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">合作异步的JavaScript:超时和间隔</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises">使用Promises:优雅的异步编程</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await">使用async和await:让异步更简单</a></li> + <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">选择正确的方法</a></li> +</ul> diff --git a/files/zh-cn/conflicting/learn/javascript/asynchronous_ae5a561b0ec11fc53c167201aa8af5df/index.html b/files/zh-cn/conflicting/learn/javascript/asynchronous_ae5a561b0ec11fc53c167201aa8af5df/index.html new file mode 100644 index 0000000000..a3500922af --- /dev/null +++ b/files/zh-cn/conflicting/learn/javascript/asynchronous_ae5a561b0ec11fc53c167201aa8af5df/index.html @@ -0,0 +1,618 @@ +--- +title: '合作异步JavaScript: 超时和间隔' +slug: conflicting/Learn/JavaScript/Asynchronous_ae5a561b0ec11fc53c167201aa8af5df +translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +original_slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +--- +<div>{{LearnSidebar}}</div> + + + +<div>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}</div> + + + +<p class="summary">在这里,我们将讨论传统的JavaScript方法,这些方法可以在一段时间或一段规则间隔(例如,每秒固定的次数)之后,以异步方式运行代码,并讨论它们的用处,以及它们的固有问题。</p> + +<table class="learn-box standard-table"> + <tbody> + <tr> + <th scope="row">预备条件:</th> + <td>基本的计算机知识,对JavaScript基本原理有较好的理解。</td> + </tr> + <tr> + <th scope="row">目标:</th> + <td>了解异步循环和间隔及其用途。</td> + </tr> + </tbody> +</table> + +<h2 id="介绍">介绍</h2> + +<p>很长一段时间以来,web平台为JavaScript程序员提供了许多函数,这些函数允许您在一段时间间隔过后异步执行代码,或者重复异步执行代码块,直到您告诉它停止为止。这些都是:</p> + +<dl> + <dt><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code></dt> + <dd>在指定的时间后执行一段代码.</dd> + <dt><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval()</a></code></dt> + <dd>以固定的时间间隔,重复运行一段代码.</dd> + <dt><code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code></dt> + <dd>setInterval()的现代版本;在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行.</dd> +</dl> + +<p>这些函数设置的异步代码实际上在主线程上运行(在其指定的计时器过去之后)。</p> + +<p>在 <code>setTimeout()</code> 调用执行之前或 <code>setInterval()</code> 迭代之间可以(并且经常会)运行其他代码。根据这些操作的处理器密集程度,它们可以进一步延迟异步代码,因为任何异步代码仅在主线程可用后才执行(换句话说,当调用栈为空时)。在阅读本文时,您将学到更多关于此问题的信息。</p> + +<p>无论如何,这些函数用于在web站点或应用程序上运行不间断的动画和其他后台处理。在下面的部分中,我们将向您展示如何使用它们。</p> + +<h2 id="setTimeout">setTimeout()</h2> + +<p>正如前述, <code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code> 在指定的时间后执行一段特定代码. 它需要如下参数:</p> + +<ul> + <li>要运行的函数,或者函数引用。</li> + <li>表示在执行代码之前等待的时间间隔(以毫秒为单位,所以1000等于1秒)的数字。如果指定值为0(或完全省略该值),函数将尽快运行(参阅下面的注释,了解为什么它“尽快”而不是“立即”运行)。稍后详述这样做的原因。</li> + <li>更多的参数:在指定函数运行时,希望传递给函数的值。</li> +</ul> + +<div class="blockIndicator note"> +<p><strong>Note:</strong> 指定的时间(或延迟)不能保证在指定的确切时间之后执行,而是最短的延迟执行时间。在主线程上的堆栈为空之前,传递给这些函数的回调将无法运行。</p> + +<p>结果,像 <code>setTimeout(fn, 0)</code> 这样的代码将在堆栈为空时立即执行,而不是立即执行。如果执行类似 <code>setTimeout(fn, 0)</code> 之类的代码,之后立即运行从 1 到 100亿 的循环之后,回调将在几秒后执行。 </p> +</div> + +<p>在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息 (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see it running live</a>, and <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see the source code</a>):</p> + +<pre class="brush: js notranslate">let myGreeting = setTimeout(function() { + alert('Hello, Mr. Universe!'); +}, 2000)</pre> + +<p>我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给 <code>setTimeout()</code> 。以下两个版本的代码片段相当于第一个版本:</p> + +<pre class="brush: js notranslate">// With a named function +let myGreeting = setTimeout(function sayHi() { + alert('Hello, Mr. Universe!'); +}, 2000) + +// With a function defined separately +function sayHi() { + alert('Hello Mr. Universe!'); +} + +let myGreeting = setTimeout(sayHi, 2000);</pre> + +<p>例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时。</p> + +<p><code>setTimeout()</code> 返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 {{anch("Clearing timeouts")}} 。</p> + +<h3 id="传递参数给setTimeout">传递参数给setTimeout() </h3> + +<p>我们希望传递给<code>setTimeout()</code>中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。</p> + +<p>例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:</p> + +<pre class="brush: js notranslate">function sayHi(who) { + alert('Hello ' + who + '!'); +}</pre> + +<p>人名可以通过第三个参数传进 <code>setTimeout()</code> :</p> + +<pre class="brush: js notranslate">let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');</pre> + +<h3 id="清除超时">清除超时</h3> + +<p>最后,如果创建了 timeout,您可以通过调用<code>clearTimeout()</code>,将<code>setTimeout()</code>调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:</p> + +<pre class="brush: js notranslate">clearTimeout(myGreeting);</pre> + +<div class="blockIndicator note"> +<p><strong>注意</strong>: 请参阅 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/greeter-app.html">greeter-app.html</a> 以获得稍微复杂一点的演示,该演示允许您在表单中设置要打招呼的人的姓名,并使用单独的按钮取消问候语(<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/greeter-app.html">see the source code also</a>)。</p> +</div> + +<h2 id="setInterval">setInterval()</h2> + +<p>当我们需要在一段时间之后运行一次代码时,<code>setTimeout()</code>可以很好地工作。但是当我们需要反复运行代码时会发生什么,例如在动画的情况下?</p> + +<p>这就是<code>setInterval()</code>的作用所在。这与<code>setTimeout()</code>的工作方式非常相似,只是作为第一个参数传递给它的函数,<strong>重复</strong>执行的时间不少于第二个参数给出的毫秒数,<strong>而不是一次执行</strong>。您还可以将正在执行的函数所需的任何参数作为 <code>setInterval()</code> 调用的后续参数传递。</p> + +<p>让我们看一个例子。下面的函数创建一个新的<code>Date()</code>对象,使用<code>toLocaleTimeString()</code>从中提取一个时间字符串,然后在UI中显示它。然后,我们使用<code>setInterval()</code>每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。</p> + +<p>(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see this live</a>, and also <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see the source</a>):</p> + +<pre class="brush: js notranslate">function displayTime() { + let date = new Date(); + let time = date.toLocaleTimeString(); + document.getElementById('demo').textContent = time; +} + +const createClock = setInterval(displayTime, 1000);</pre> + +<p><code><font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">像</span></font>setTimeout()一样</code>, <code>setInterval()</code> 返回一个确定的值,稍后你可以用它来取消间隔任务。</p> + +<h3 id="清除intervals">清除intervals</h3> + +<p><code>setInterval</code>()永远保持运行任务,除非我们做点什么——我们可能会想阻止这样的任务,否则当浏览器无法完成任何进一步的任务时我们可能得到错误, 或者动画处理已经完成了。我们可以用与停止超时相同的方法来实现这一点——通过将<code>setInterval</code>()调用返回的标识符传递给<code>clearInterval</code>()函数:</p> + +<pre class="brush: js notranslate">const myInterval = setInterval(myFunction, 2000); + +clearInterval(myInterval);</pre> + +<h4 id="主动学习:创建秒表!">主动学习:创建秒表!</h4> + +<p>话虽如此,我们还是要给你一个挑战。以我们的<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">setInterval-clock.html</a>为例,修改它以创建您自己的简单秒表。</p> + +<p>你要像前面一样显示时间,但是在这里,你需要:</p> + +<ul> + <li>"Start" 按钮开始计时.</li> + <li>"Stop" 按钮暂停/停止计时</li> + <li>"Reset" 按钮清零.</li> + <li>时间显示经过的秒数.</li> +</ul> + +<p>提示:</p> + +<ul> + <li>您可以随心所欲地构造按钮标记;只需确保使用HTML语法,确保JavaScript获取按钮引用。</li> + <li>创建一个变量,从0开始,每秒钟增加1.</li> + <li>与我们版本中所做的一样,在不使用<code>Date</code>()对象的情况下创建这个示例更容易一些,但是不那么精确——您不能保证回调会在恰好1000ms之后触发。更准确的方法是运行<code>startTime = Date.now()</code>来获取用户单击start按钮的确切时间戳,然后执行<code>Date.now() - startTime</code>来获取单击<code>start</code>按钮后的毫秒数。</li> + <li>您还希望将小时、分钟和秒作为单独的值计算,然后在每次循环迭代之后将它们一起显示在一个字符串中。从第二个计数器,你可以计算出他们.</li> + <li>如何计时时分秒? 想一下: + <ul> + <li>一小时3600秒.</li> + <li>分钟应该是:时间减去小时后剩下的除以60.</li> + <li>分钟都减掉后,剩下的就是秒钟了.</li> + </ul> + </li> + <li>如果这个数字小于10,最好在显示的值前面加一个0,这样看起来更加像那么回事。</li> + <li>暂停的话,要清掉间隔函数。重置的话,显示要清零,要更新显示</li> +</ul> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: 如果您在操作过程有困难,请参考 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-stopwatch.html">see it runing live</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-stopwatch.html">source code</a> also).</p> +</div> + +<h2 id="关于_setTimeout_和_setInterval_需要注意的几点">关于 setTimeout() 和 setInterval() 需要注意的几点</h2> + +<p>当使用 <code>setTimeout()</code> 和 <code>setInterval()</code>的时候,有几点需要额外注意。 现在让我们回顾一下:</p> + +<h3 id="递归的timeouts">递归的timeouts</h3> + +<p>还有另一种方法可以使用<code>setTimeout()</code>:我们可以递归调用它来重复运行相同的代码,而不是使用<code>setInterval()</code>。</p> + +<p>下面的示例使用递归<code>setTimeout()</code>每100毫秒运行传递来的函数:</p> + +<pre class="brush: js notranslate">let i = 1; + +setTimeout(function run() { + console.log(i); + i++; + setTimeout(run, 100); +}, 100);</pre> + +<p>将上面的示例与下面的示例进行比较 ––这使用 <code>setInterval()</code> 来实现相同的效果:</p> + +<pre class="brush: js notranslate">let i = 1; + +setInterval(function run() { + console.log(i); + i++ +}, 100);</pre> + +<h4 id="递归setTimeout和setInterval有何不同?">递归setTimeout()和setInterval()有何不同?</h4> + +<p>上述代码的两个版本之间的差异是微妙的。</p> + +<ul> + <li>递归 <code>setTimeout()</code> 保证执行之间的延迟相同,例如在上述情况下为100ms。 代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。</li> + <li>使用 <code>setInterval()</code> 的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 - 然后间隔最终只有60毫秒。</li> + <li>当递归使用 <code>setTimeout()</code> 时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。 换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。</li> +</ul> + +<p>当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的 <code>setTimeout()</code> - 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。</p> + +<h3 id="立即超时">立即超时</h3> + +<p>使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。</p> + +<p>举个例子,下面的代码(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/zero-settimeout.html">see it live</a>) 输出一个包含警报的"Hello",然后在您点击第一个警报的OK之后立即弹出“world”。</p> + +<pre class="brush: js notranslate">setTimeout(function() { + alert('World'); +}, 0); + +alert('Hello');</pre> + +<p><font><font>如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。</font></font></p> + +<h3 id="使用_clearTimeout_or_clearInterval清除">使用 clearTimeout() or clearInterval()清除</h3> + +<p><code>clearTimeout()</code> 和<code>clearInterval()</code> 都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除 <code>setTimeout()</code> 和 <code>setInterval()。</code></p> + +<p>但为了保持一致性,你应该使用 <code>clearTimeout()</code> 来清除 <code>setTimeout()</code> 条目,使用 <code>clearInterval()</code> 来清除 <code>setInterval()</code> 条目。 这样有助于避免混乱。</p> + +<h2 id="requestAnimationFrame">requestAnimationFrame()</h2> + +<p><code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code> 是一个专门的循环函数,旨在浏览器中高效运行动画。它基本上是现代版本的<code>setInterval()</code> —— 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行,不管其运行的环境如何。</p> + +<p>它是针对<code>setInterval()</code> 遇到的问题创建的,比如 <code>setInterval()</code>并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。</p> + +<p>(在<a href="http://creativejs.com/resources/requestanimationframe/index.html">CreativeJS</a>上了解有关此内容的更多信息).</p> + +<div class="blockIndicator note"> +<p><strong>注意:</strong> 你可以在课程中其他地方找到<code>requestAnimationFrame()</code> 的使用范例—参见 <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Drawing graphics</a>, 和 <a href="/en-US/docs/Learn/JavaScript/Objects/Object_building_practice">Object building practice</a>。</p> +</div> + +<p><font>该方法将重新加载页面之前要调用的回调函数作为参数。</font><font>这是您将看到的常见表达:</font></p> + +<pre class="brush: js notranslate">function draw() { + // Drawing code goes here + requestAnimationFrame(draw); +} + +draw();</pre> + +<p>这个想法是要定义一个函数,在其中更新动画 (例如,移动精灵,更新乐谱,刷新数据等),然后调用它来开始这个过程。在函数的末尾,以 <code>requestAnimationFrame()</code> 传递的函数作为参数进行调用,这指示浏览器在下一次显示重新绘制时再次调用该函数。然后这个操作连续运行, 因为<code>requestAnimationFrame()</code> 是递归调用的。</p> + +<div class="blockIndicator note"> +<p><strong>注意</strong>: 如果要执行某种简单的常规DOM动画, <a href="/en-US/docs/Web/CSS/CSS_Animations">CSS 动画</a> 可能更快,因为它们是由浏览器的内部代码计算而不是JavaScript直接计算的。但是,如果您正在做一些更复杂的事情,并且涉及到在DOM中不能直接访问的对象(such as <a href="/en-US/docs/Web/API/Canvas_API">2D Canvas API</a> or <a href="/en-US/docs/Web/API/WebGL_API">WebGL</a> objects), <code>requestAnimationFrame()</code> 在大多数情况下是更好的选择。</p> +</div> + +<h3 id="你的动画跑得有多快?">你的动画跑得有多快?</h3> + +<p>动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps)为单位进行测量。这个数字越高,动画看起来就越平滑。</p> + +<p>由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。</p> + +<p>如果您有一个刷新率为60Hz的显示器,并且希望达到60fps,则大约有16.7毫秒(1000/60)来执行动画代码来渲染每个帧。这提醒我们,我们需要注意每次通过动画循环时要运行的代码量。</p> + +<p><code>requestAnimationFrame()</code> 总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。<code>requestAnimationFrame()</code> 会尽其所能利用现有资源提升帧速率。</p> + +<h3 id="requestAnimationFrame_与_setInterval_和_setTimeout有什么不同"> requestAnimationFrame() 与 setInterval() 和 setTimeout()有什么不同?</h3> + +<p>让我们进一步讨论一下 <code>requestAnimationFrame()</code> 方法与前面介绍的其他方法的区别. 下面让我们看一下代码:</p> + +<pre class="brush: js notranslate">function draw() { + // Drawing code goes here + requestAnimationFrame(draw); +} + +draw();</pre> + +<p>现在让我们看看如何使用<code>setInterval()</code>:</p> + +<pre class="brush: js notranslate">function draw() { + // Drawing code goes here +} + +setInterval(draw, 17);</pre> + +<p>如前所述,我们没有为<code>requestAnimationFrame()</code>;指定时间间隔;它只是在当前条件下尽可能快速平稳地运行它。如果动画由于某些原因而处于屏幕外浏览器也不会浪费时间运行它。</p> + +<p> 另一方面<code>setInterval()</code>需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。四舍五入是一个好主意,浏览器可能会尝试运行动画的速度超过60fps,它不会对动画的平滑度有任何影响。如前所述,60Hz是标准刷新率。</p> + +<h3 id="包括时间戳">包括时间戳</h3> + +<p>传递给 <code>requestAnimationFrame()</code> 函数的实际回调也可以被赋予一个参数(一个时间戳值),表示自 <code>requestAnimationFrame()</code> 开始运行以来的时间。这是很有用的,因为它允许您在特定的时间以恒定的速度运行,而不管您的设备有多快或多慢。您将使用的一般模式如下所示:</p> + +<pre class="brush: js notranslate">let startTime = null; + +function draw(timestamp) { + if(!startTime) { + startTime = timestamp; + } + + currentTime = timestamp - startTime; + + // Do something based on current time + + requestAnimationFrame(draw); +} + +draw();</pre> + +<h3 id="浏览器支持">浏览器支持</h3> + +<p> 与<code>setInterval()<font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">或</span></font></code><code>setTimeout()</code> 相比最近的浏览器支持<code>requestAnimationFrame()</code></p> + +<p>— <code>requestAnimationFrame()</code>.在Internet Explorer 10及更高版本中可用。因此,除非您的代码需要支持旧版本的IE,否则没有什么理由不使用<code>requestAnimationFrame()</code> 。</p> + +<h3 id="一个简单的例子">一个简单的例子</h3> + +<p>学习上述理论已经足够了,下面让我们仔细研究并构建自己的<code>requestAnimationFrame()</code> 示例。我们将创建一个简单的“旋转器动画”(spinner animation),即当应用程序忙于连接到服务器时可能会显示的那种动画。</p> + +<div class="blockIndicator note"> +<p><strong>注意</strong>: 一般来说,像以下例子中如此简单的动画应用CSS动画来实现,这里使用<code>requestAnimationFrame()</code>只是为了帮助解释其用法。<code>requestAnimationFrame()</code>正常应用于如逐帧更新游戏画面这样的复杂动画。</p> +</div> + +<ol> + <li> + <p>首先, 下载我们的<a href="https://github.com/mdn/learning-area/blob/master/html/introduction-to-html/getting-started/index.html">网页模板</a>。</p> + </li> + <li> + <p>放置一个空的 {{htmlelement("div")}} 元素进入 {{htmlelement("body")}}, 然后在其中加入一个 ↻ 字符.这是一个将循环的字符将在我们的例子中作为我们的旋转器(spinner)。</p> + </li> + <li> + <p>用任何你喜欢的方法应用下述的CSS到HTML模板中。这些在页面上设置了一个红色背景,将<code><body></code>的高度设置为100%<code><html></code>的高度,并将<code><div></code>水平和竖直居中。</p> + + <pre class="brush: html notranslate">html { + background-color: white; + height: 100%; +} + +body { + height: inherit; + background-color: red; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} + +div { + display: inline-block; + font-size: 10rem; +}</pre> + </li> + <li> + <p>插入一个 {{htmlelement("script")}}元素在 <code></body> </code>标签之上。</p> + </li> + <li> + <p>插入下述的JavaScript在你的 <code><script> </code>元素中。这里我们存储了一个<div>的引用在一个常量中,设置<code>rotateCount</code>变量为 0, 设置一个未初始化的变量之后将会用作容纳一个<code>requestAnimationFrame()</code> 的调用, 然后设置一个 <code>startTime</code> 变量为 <code>null</code>,它之后将会用作存储 <code>requestAnimationFrame()</code> 的起始时间.。</p> + + <pre class="brush: js notranslate">const spinner = document.querySelector('div'); +let rotateCount = 0; +let rAF; +let startTime = null;</pre> + </li> + <li> + <p>在之前的代码下面, 插入一个 <code>draw()</code> 函数将被用作容纳我们的动画代码,并且包含了 <code>时间戳</code> 参数。</p> + + <pre class="brush: js notranslate">function draw(timestamp) { + +}</pre> + </li> + <li> + <p>在<code>draw()</code>中, 加入下述的几行。 如果起始时间还没有被赋值的话,将<code>timestamp</code> 传给它(这只将发生在循环中的第一步)。 并赋值给<code>rotateCount</code> ,以旋转 旋转器(spinning)(此处取<code>(当前时间戳 – 起始时间戳) / 3</code>,以免它转得太快):</p> + + <pre class="brush: js notranslate"> if (!startTime) { + startTime = timestamp; + } + + rotateCount = (timestamp - startTime) / 3; +</pre> + </li> + <li> + <p>在<code>draw()</code>内我们刚刚添加的代码之后,添加以下代码 — 此处是在检查<code>rotateCount</code> 的值是否超过了<code>359</code> (e.g. <code>360</code>, 一个正圆的度数)。 如果是,与360取模(值除以 <code>360</code> 后剩下的余数),使得圆圈的动画能以合理的低值连续播放。需要注意的是,这样的操作并不是必要的,只是比起类似于“128000度”的值,运行在 0-359 度之间会使你的操作更容易些。</p> + + <pre class="brush: js notranslate">if (rotateCount > 359) { + rotateCount %= 360; +}</pre> + </li> + <li> + <p>接下来,在上一个块下面添加以下行以实际旋转 旋转(spinner)器:</p> + + <pre class="notranslate">spinner.style.transform = `rotate(${rotateCount}deg)`;</pre> + </li> + <li> + <p>在函数<code>draw()</code>内的最下方,插入下面这行代码。这是整个操作中最关键的部分 — 我们将前面定义的变量设置为以<code>draw()</code>函数为参数的活动<code>requestAnimation()</code>调用。 这样动画就开始了,以尽可能接近60 FPS的速率不断地运行<code>draw()</code>函数。</p> + + <pre class="notranslate">rAF = requestAnimationFrame(draw);</pre> + </li> + <li> + <p>在 <code>draw()</code> 函数定义下方,添加对 <code>draw()</code> 函数的调用以启动动画。</p> + </li> + <li> + <pre class="notranslate">draw();</pre> + </li> +</ol> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: You can find this <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">example live on GitHub</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">source code</a> also).</p> +</div> + +<h3 id="撤销requestAnimationFrame">撤销requestAnimationFrame()</h3> + +<p><code>requestAnimationFrame()</code>可用与之对应的<code>cancelAnimationFrame()</code>方法“撤销”(不同于“set…”类方法的“清除”,此处更接近“撤销”之意)。</p> + +<p>该方法以<code>requestAnimationFrame()</code>的返回值为参数,此处我们将该返回值存在变量 <code>rAF</code> 中:</p> + +<pre class="brush: js notranslate">cancelAnimationFrame(rAF);</pre> + +<h3 id="主动学习:_启动和停止旋转器(spinner)">主动学习: 启动和停止旋转器(spinner)</h3> + +<p>在这个练习中,我们希望你能对<code>cancelAnimationFrame()</code>方法做一些测试,在之前的示例中添加新内容。你需要在示例中添加一个事件监听器,用于当鼠标在页面任意位置点击时,启动或停止旋转器。 </p> + +<p>一些提示:</p> + +<ul> + <li>包含<code><body></code>在内大多数元素都可以添加<code>click</code>事件处理器<font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">。为了使可点击区域最大化,直接监听</span></font><code><body></code>元素的事件是更有效的,因为事件可以冒泡到其子元素。</li> + <li>你可能需要添加一个追踪用变量来检测旋转器是否在旋转。该变量若为真,则清除动画帧;若为假,则重新调用函数。</li> +</ul> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: 先自己尝试一下,如果你确实遇到困难,可以参看我们的<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/start-and-stop-spinner.html">在线示例</a>和<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/start-and-stop-spinner.html">源代码</a>。</p> +</div> + +<h3 id="限制_节流_requestAnimationFrame动画">限制 (节流) requestAnimationFrame()动画</h3> + +<p>requestAnimationFrame() 的限制之一是无法选择帧率。在大多数情况下,这不是问题,因为通常希望动画尽可能流畅地运行。但是,当要创建老式的8位类型的动画时,怎么办?</p> + +<p>例如,在我们的 <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Drawing Graphics</a> 文章中的猴子岛行走动画中的一个问题:</p> + +<p>{{EmbedGHLiveSample("learning-area/javascript/apis/drawing-graphics/loops_animation/7_canvas_walking_animation.html", '100%', 260)}}</p> + +<p>在此示例中,必须为角色在屏幕上的位置及显示的精灵设置动画。精灵动画中只有6帧。如果通过 <code>requestAnimationFrame()</code> 为屏幕上显示的每个帧显示不同的精灵帧,则 Guybrush 的四肢移动太快,动画看起来很荒谬。因此,此示例使用以下代码限制精灵循环的帧率:</p> + +<pre class="brush: js notranslate">if (posX % 13 === 0) { + if (sprite === 5) { + sprite = 0; + } else { + sprite++; + } +}</pre> + +<p>因此,代码每13个动画帧循环一次精灵。</p> + +<p>...实际上,大约是每 6.5 帧,因为我们将每帧更新 <code>posX</code> 2个单位值(角色在屏幕上的位置)</p> + +<pre class="brush: js notranslate">if(posX > width/2) { + newStartPos = -( (width / 2) + 102 ); + posX = Math.ceil(newStartPos / 13) * 13; + console.log(posX); +} else { + posX += 2; +}</pre> + +<p>这是用于计算如何更新每个动画帧中的位置的代码。</p> + +<p>用于限制动画的方法将取决于特定代码。例如,在前面的旋转器实例中,可以通过仅在每帧中增加 rotateCount 一个单位(而不是两个单位)来使其运动速度变慢。</p> + +<h2 id="主动学习:反应游戏">主动学习:反应游戏</h2> + +<p>对于本文的最后部分,将创建一个 2 人反应游戏。游戏将有两个玩家,其中一个使用 <kbd>A</kbd> 键控制游戏,另一个使用 <kbd>L</kbd> 键控制游戏。</p> + +<p>按下 <em>开始</em> 按钮后,像前面看到的旋转器那样的将显示 5 到 10 秒之间的随机时间。之后将出现一条消息,提示 “PLAYERS GO!!”。一旦这发生,第一个按下其控制按钮的玩家将赢得比赛。</p> + +<p>{{EmbedGHLiveSample("learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html", '100%', 500)}}</p> + +<p>让我们来完成以下工作:</p> + +<ol> + <li> + <p>首先,下载 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/reaction-game-starter.html">starter file for the app</a> 。其中包含完成的 HTML 结构和 CSS 样式,为我们提供了一个游戏板,其中显示了两个玩家的信息(如上所示),但 spinner 和 结果段落重叠显示,只需要编写 JavaScript 代码。</p> + </li> + <li> + <p>在页面上空的 {{htmlelement("script")}} 元素里,首先添加以下几行代码,这些代码定义了其余代码中需要的一些常量和变量:</p> + + <pre class="brush: js notranslate">const spinner = document.querySelector('.spinner p'); +const spinnerContainer = document.querySelector('.spinner'); +let rotateCount = 0; +let startTime = null; +let rAF; +const btn = document.querySelector('button'); +const result = document.querySelector('.result');</pre> + + <p>这些是:</p> + + <ol> + <li>对旋转器的引用,因此可以对其进行动画处理。</li> + <li>包含旋转器 {{htmlelement("div")}} 元素的引用。用于显示和隐藏它。</li> + <li>旋转计数。这确定了显示在动画每一帧上的旋转器旋转了多少。</li> + <li>开始时间为null。当旋转器开始旋转时,它将赋值为 开始时间。</li> + <li>一个未初始化的变量,用于之后存储使 旋转器 动画化的 {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} 调用。</li> + <li>开始按钮的引用。</li> + <li>结果字段的引用。</li> + </ol> + </li> + <li> + <p>接下来,在前几行代码下方,添加以下函数。它只接收两个数字并返回一个在两个数字之间的随机数。稍后将需要它来生成随机超时间隔。</p> + + <pre class="brush: js notranslate">function random(min,max) { + var num = Math.floor(Math.random()*(max-min)) + min; + return num; +}</pre> + </li> + <li> + <p>接下来添加 <code>draw()</code> 函数以使 旋转器 动画化。这与之前的简单旋转器示例中的版本非常相似:</p> + + <pre class="brush: js notranslate"> function draw(timestamp) { + if(!startTime) { + startTime = timestamp; + } + + let rotateCount = (timestamp - startTime) / 3; + + if(rotateCount > 359) { + rotateCount %= 360; + } + + spinner.style.transform = 'rotate(' + rotateCount + 'deg)'; + rAF = requestAnimationFrame(draw); + }</pre> + </li> + <li> + <p>现在是时候在页面首次加载时设置应用程序的初始状态了。添加以下两行,它们使用 <code>display: none;</code> 隐藏结果段落和旋转器容器。</p> + + <pre class="brush: js notranslate">result.style.display = 'none'; +spinnerContainer.style.display = 'none';</pre> + </li> + <li> + <p>接下来,定义一个 <code>reset()</code> 函数。该函数在游戏结束后将游戏设置回初始状态以便再次开启游戏。在代码底部添加以下内容:</p> + + <pre class="brush: js notranslate">function reset() { + btn.style.display = 'block'; + result.textContent = ''; + result.style.display = 'none'; +}</pre> + </li> + <li>好的,准备充分!现在该使游戏变得可玩了!将以下代码块添加到代码中。 <code>start()</code> 函数调用 <code>draw()</code> 以启动 旋转器,并在UI中显示它,隐藏“开始”按钮,这样您就无法通过同时启动多次来弄乱游戏,并运行一个经过5到10秒的随机间隔后,会运行 <code>setEndgame()</code> 函数的 <code>setTimeout()</code> 。下面的代码块还将一个事件侦听器添加到按钮上,以在单击它时运行 <code>start()</code> 函数。 + <pre class="notranslate">btn.addEventListener('click', start); + +function start() { + draw(); + spinnerContainer.style.display = 'block'; + btn.style.display = 'none'; + setTimeout(setEndgame, random(5000,10000)); +}</pre> + </li> + <li>添加以下方法到代码:</li> +</ol> + +<pre class="brush: js notranslate">function setEndgame() { + cancelAnimationFrame(rAF); + spinnerContainer.style.display = 'none'; + result.style.display = 'block'; + result.textContent = 'PLAYERS GO!!'; + + document.addEventListener('keydown', keyHandler); + + function keyHandler(e) { + console.log(e.key); + if(e.key === 'a') { + result.textContent = 'Player 1 won!!'; + } else if(e.key === 'l') { + result.textContent = 'Player 2 won!!'; + } + + document.removeEventListener('keydown', keyHandler); + setTimeout(reset, 5000); + }; +}</pre> + +<p>逐步执行以下操作:</p> + +<ol> + <li>首先通过 {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}} 取消 旋转器 动画(清理不必要的流程总是一件好事),隐藏 旋转器 容器。</li> + <li>接下来,显示结果段落并将其文本内容设置为“ PLAYERS GO !!”。向玩家发出信号,表示他们现在可以按下按钮来取胜。</li> + <li>将 <code><a href="/en-US/docs/Web/API/Document/keydown_event">keydown</a></code> 事件侦听器附加到 document。当按下任何按钮时,<code>keyHandler()</code> 函数将运行。</li> + <li>在 <code>keyHandler()</code> 里,在 <code>keyHandler()</code> 内部,代码包括作为参数的事件对象作为参数(用 <code>e</code> 表示)- 其 {{domxref("KeyboardEvent.key", "key")}} 属性包含刚刚按下的键,可以通过这个对象来对特定的操作和特定的按键做出响应。</li> + <li>将变量 <code>isOver</code> 设置为 <code>false</code> ,这样我们就可以跟踪是否按下了正确的按键以使玩家1或2获胜。我们不希望游戏在按下错误的键后结束。</li> + <li>将 <code>e.key</code> 输出到控制台,这是找出所按的不同键的键值的有用方法。</li> + <li>当 <code>e.key</code> 为“ a”时,显示一条消息说玩家1获胜;当 <code>e.key</code> 为“ l”时,显示消息说玩家2获胜。 (注意:这仅适用于小写的a和l - 如果提交了大写的 A 或 L(键加上 <kbd>Shift</kbd>),则将其视为另一个键!)如果按下了其中一个键,请将 <code>isOver</code> 设置为 <code>true</code> 。</li> + <li>仅当 <code>isOver</code> 为 <code>true</code> 时,才使用 {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} 删除 <code>keydown</code> 事件侦听器,以便一旦产生获胜的按键,就不再有键盘输入可以弄乱最终游戏结果。您还可以使用 <code>setTimeout()</code> 在5秒钟后调用 <code>reset()</code>-如前所述,此函数将游戏重置为原始状态,以便可以开始新游戏。</li> +</ol> + +<p>就这样-一切都完成了!</p> + +<div class="blockIndicator note"> +<p><strong>Note</strong>: 如果卡住了, check out <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html">our version of the reaction game</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/reaction-game.html">source code</a> also).</p> +</div> + +<h2 id="结论">结论</h2> + +<p>就是这样-异步循环和间隔的所有要点在一篇文章中介绍了。您会发现这些方法在许多情况下都很有用,但请注意不要过度使用它们!因为它们仍然在主线程上运行,所以繁重的回调(尤其是那些操纵DOM的回调)会在不注意的情况下降低页面的速度。</p> + +<p>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}</p> + +<h2 id="In_this_module">In this module</h2> + +<ul> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Concepts">General asynchronous programming concepts</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">Introducing asynchronous JavaScript</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Cooperative asynchronous JavaScript: Timeouts and intervals</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises">Graceful asynchronous programming with Promises</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Async_await">Making asynchronous programming easier with async and await</a></li> + <li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">Choosing the right approach</a></li> +</ul> |