From 310fd066e91f454b990372ffa30e803cc8120975 Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Thu, 11 Feb 2021 12:56:40 +0100 Subject: unslug zh-cn: move --- .../javascript/asynchronous/async_await/index.html | 379 +++++++++++++ .../choosing_the_right_approach/index.html | 523 +++++++++++++++++ .../javascript/asynchronous/concepts/index.html | 162 ++++++ .../zh-cn/learn/javascript/asynchronous/index.html | 59 ++ .../javascript/asynchronous/introducing/index.html | 272 +++++++++ .../javascript/asynchronous/promises/index.html | 599 ++++++++++++++++++++ .../asynchronous/timeouts_and_intervals/index.html | 617 +++++++++++++++++++++ 7 files changed, 2611 insertions(+) create mode 100644 files/zh-cn/learn/javascript/asynchronous/async_await/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/choosing_the_right_approach/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/concepts/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/introducing/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/promises/index.html create mode 100644 files/zh-cn/learn/javascript/asynchronous/timeouts_and_intervals/index.html (limited to 'files/zh-cn/learn/javascript/asynchronous') diff --git a/files/zh-cn/learn/javascript/asynchronous/async_await/index.html b/files/zh-cn/learn/javascript/asynchronous/async_await/index.html new file mode 100644 index 0000000000..739ab63602 --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/async_await/index.html @@ -0,0 +1,379 @@ +--- +title: 'async和await:让异步编程更简单' +slug: learn/JavaScript/异步/Async_await +translation_of: Learn/JavaScript/Asynchronous/Async_await +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}
+ +

async functions 和 await 关键字是最近添加到JavaScript语言里面的。它们是ECMAScript 2017 JavaScript版的一部分(参见ECMAScript Next support in Mozilla)。简单来说,它们是基基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。本文为您提供了您需要了解的内容。

+ + + + + + + + + + + + +
先决条件:基本的计算机知识,较好理解 JavaScript 基础,以及理解一般异步代码和 promises 。
目标:理解并使用 promise
+ +

async/await 基础

+ +

在代码中使用 async / await 有两个部分。

+ +

async 关键字

+ +

首先,我们使用 async 关键字,把它放在函数声明之前,使其成为 async function。异步函数是一个知道怎样使用 await 关键字调用异步代码的函数。

+ +

尝试在浏览器的JS控制台中键入以下行:

+ +
function hello() { return "Hello" };
+hello();
+ +

该函数返回“Hello” —— 没什么特别的,对吧?

+ +

如果我们将其变成异步函数呢?请尝试以下方法:

+ +
async function hello() { return "Hello" };
+hello();
+ +

哈。现在调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。

+ +

你也可以创建一个异步函数表达式(参见 async function expression ),如下所示:

+ +
let hello = async function() { return "Hello" };
+hello();
+ +

你可以使用箭头函数:

+ +
let hello = async () => { return "Hello" };
+ +

这些都基本上是一样的。

+ +

要实际使用promise完成时返回的值,我们可以使用.then()块,因为它返回的是 promise:

+ +
hello().then((value) => console.log(value))
+ +

甚至只是简写如

+ +
hello().then(console.log)
+ +

这就像我们在上一篇文章中看到的那样。

+ +

将 async 关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用 await 带来的任何潜在开销。在函数声明为 async 时,JavaScript引擎会添加必要的处理,以优化你的程序。爽!

+ +

await关键字

+ +

当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。

+ +

您可以在调用任何返回Promise的函数时使用 await,包括Web API函数。

+ +

这是一个简单的示例:

+ +
async function hello() {
+  return greeting = await Promise.resolve("Hello");
+};
+
+hello().then(alert);
+ +

当然,上面的示例不是很有用,但它确实展示了语法。让我们继续,看一个真实示例。

+ +

 使用 async/await 重写 promise 代码

+ +

让我们回顾一下我们在上一篇文章中简单的 fetch 示例:

+ +
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);
+});
+ +

到现在为止,你应该对 promises 及其工作方式有一个较好的理解。让我们将其转换为使用async / await看看它使事情变得简单了多少:

+ +
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);
+});
+ +

它使代码简单多了,更容易理解 —— 去除了到处都是 .then() 代码块!

+ +

由于 async 关键字将函数转换为 promise,您可以重构以上代码 —— 使用 promise 和 await 的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:

+ +
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);
+});
+ +

您可以尝试自己输入示例,或运行我们的 live example (另请参阅source code)。

+ +

它到底是如何工作的?

+ +

您会注意到我们已经将代码封装在函数中,并且我们在 function 关键字之前包含了 async 关键字。这是必要的 –– 您必须创建一个异步函数来定义一个代码块,在其中运行异步代码; await 只能在异步函数内部工作。

+ +

myFetch()函数定义中,您可以看到代码与先前的 promise 版本非常相似,但存在一些差异。不需要附加 .then() 代码块到每个promise-based方法的结尾,你只需要在方法调用前添加 await 关键字,然后把结果赋给变量。await 关键字使JavaScript运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,您的代码将继续从下一行开始执行。例如:

+ +
let response = await fetch('coffee.jpg');
+ +

解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 fetch() 返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 response 变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个Blob。Blob这行也调用基于异步promise的方法,因此我们也在此处使用await。当操作结果返回时,我们将它从myFetch()函数中返回。

+ +

这意味着当我们调用myFetch()函数时,它会返回一个promise,因此我们可以将.then()链接到它的末尾,在其中我们处理显示在屏幕上的blob

+ +

你可能已经觉得“这真的很酷!”,你是对的 —— 用更少的.then()块来封装代码,同时它看起来很像同步代码,所以它非常直观。

+ +

添加错误处理

+ +

如果你想添加错误处理,你有几个选择。

+ +

您可以将同步的 try...catch 结构和 async/await 一起使用 。此示例扩展了我们上面展示的第一个版本代码:

+ +
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();
+ +

catch() {} 代码块会接收一个错误对象 e ; 我们现在可以将其记录到控制台,它将向我们提供详细的错误消息,显示错误被抛出的代码中的位置。

+ +

如果你想使用我们上面展示的第二个(重构)代码版本,你最好继续混合方式并将 .catch() 块链接到 .then() 调用的末尾,就像这样:

+ +
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)
+);
+ +

这是因为 .catch() 块将捕获来自异步函数调用和promise链中的错误。如果您在此处使用了try/catch 代码块,则在调用 myFetch() 函数时,您仍可能会收到未处理的错误。

+ +

您可以在GitHub上找到这两个示例:

+ + + +

等待Promise.all()

+ +

async / await 建立在 promises 之上,因此它与promises提供的所有功能兼容。这包括Promise.all() –– 你完全可以通过调用 await Promise.all() 将所有结果返回到变量中,就像同步代码一样。让我们再次回到上一篇中看到的例子。在单独的选项卡中打开它,以便您可以与下面显示的新版本进行比较和对比。

+ +

将其转换为 async / await(请参阅 样例 和 源码),现在看起来像这样:

+ +
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)
+);
+ +

可以看到 fetchAndDecode() 函数只进行了一丁点的修改就转换成了异步函数。请看Promise.all() 行:

+ +
let values = await Promise.all([coffee, tea, description]);
+ +

在这里,通过使用await,我们能够在三个promise的结果都可用的时候,放入values数组中。这看起来非常像同步代码。我们需要将所有代码封装在一个新的异步函数displayContent() 中,尽管没有减少很多代码,但能够将大部分代码从 .then() 代码块移出,使代码得到了简化,更易读。

+ +

为了错误处理,我们在 displayContent() 调用中包含了一个 .catch() 代码块;这将处理两个函数中出现的错误。

+ +
+

注意: 也可以在异步函数中使用同步 finally 代码块代替 .finally() 异步代码块,以显示操作如何进行的最终报告——您可以在我们的 live example (查看源代码)中看到这一点。

+
+ +

async/await的缺陷

+ +

了解Async/await是非常有用的,但还有一些缺点需要考虑。

+ +

Async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。

+ +

这意味着您的代码可能会因为大量await的promises相继发生而变慢。每个await都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await时那样)。

+ +

有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。

+ +

我们有两个可用的例子 —— slow-async-await.html(参见source code)和fast-async-await.html(参见source code)。它们都以自定义promise函数开始,该函数使用setTimeout() 调用伪造异步进程:

+ +
function timeoutPromise(interval) {
+  return new Promise((resolve, reject) => {
+    setTimeout(function(){
+      resolve("done");
+    }, interval);
+  });
+};
+ +

然后每个包含一个 timeTest() 异步函数,等待三个 timeoutPromise() 调用:

+ +
async function timeTest() {
+  ...
+}
+ +

每一个都以记录开始时间结束,查看 timeTest() promise 需要多长时间才能完成,然后记录结束时间并报告操作总共需要多长时间:

+ +
let startTime = Date.now();
+timeTest().then(() => {
+  let finishTime = Date.now();
+  let timeTaken = finishTime - startTime;
+  alert("Time taken in milliseconds: " + timeTaken);
+})
+ +

timeTest() 函数在每种情况下都不同。

+ +

slow-async-await.html示例中,timeTest() 如下所示:

+ +
async function timeTest() {
+  await timeoutPromise(3000);
+  await timeoutPromise(3000);
+  await timeoutPromise(3000);
+}
+ +

在这里,我们直接等待所有三个timeoutPromise()调用,使每个调用3秒钟。后续的每一个都被迫等到最后一个完成 - 如果你运行第一个例子,你会看到弹出框报告的总运行时间大约为9秒。

+ +

fast-async-await.html示例中,timeTest() 如下所示:

+ +
async function timeTest() {
+  const timeoutPromise1 = timeoutPromise(3000);
+  const timeoutPromise2 = timeoutPromise(3000);
+  const timeoutPromise3 = timeoutPromise(3000);
+
+  await timeoutPromise1;
+  await timeoutPromise2;
+  await timeoutPromise3;
+}
+ +

在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。

+ +

接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!

+ +

您必须仔细测试您的代码,并在性能开始受损时牢记这一点。

+ +

另一个小小的不便是你必须将等待执行的promise封装在异步函数中。

+ +

Async/await 的类方法

+ +

最后值得一提的是,我们可以在类/对象方法前面添加async,以使它们返回promises,并await它们内部的promises。查看 ES class code we saw in our object-oriented JavaScript article,然后查看使用异步方法的修改版本:

+ +
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']);
+ +

第一个实例方法可以使用如下:

+ +
han.greeting().then(console.log);
+ +

浏览器的支持

+ +

决定是否使用 async/await 时的一个考虑因素是支持旧浏览器。它们适用于大多数浏览器的现代版本,与promise相同; 主要的支持问题存在于Internet Explorer和Opera Mini。

+ +

如果你想使用async/await但是担心旧的浏览器支持,你可以考虑使用BabelJS库 —— 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。在遇到不支持async/await 的浏览器时,Babel的 polyfill 可以自动提供适用于旧版浏览器的实现。

+ +

总结

+ +

async/await提供了一种很好的,简化的方法来编写更易于阅读和维护的异步代码。即使浏览器支持在撰写本文时比其他异步代码机制更受限制,但无论是现在还是将来,都值得学习和考虑使用。

+ +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}

+ +

本章内容

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/choosing_the_right_approach/index.html b/files/zh-cn/learn/javascript/asynchronous/choosing_the_right_approach/index.html new file mode 100644 index 0000000000..276d815b85 --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/choosing_the_right_approach/index.html @@ -0,0 +1,523 @@ +--- +title: 选择正确的方法 +slug: learn/JavaScript/异步/Choosing_the_right_approach +translation_of: Learn/JavaScript/Asynchronous/Choosing_the_right_approach +--- +
{{LearnSidebar}}
+ +
{{PreviousMenu("Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}
+ +

为了完成这个模块,我们将简要讨论之前章节谈论过编码技术和功能,看看你应该使用哪一个,并提供适当的建议和提醒。随着时间的推移,我们可能会添加到此资源中。

+ + + + + + + + + + + + +
预备条件:基本的计算机素养,对JavaScript基础知识的合理理解。
目标:能够在使用不同的异步编程技术时做出合理的选择。
+ +

异步回调

+ +

通常在旧式API中找到,涉及将函数作为参数传递给另一个函数,然后在异步操作完成时调用该函数,以便回调可以依次对结果执行某些操作。这是promise的前身;它不那么高效或灵活。仅在必要时使用。

+ + + + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoYes (recursive callbacks)Yes (nested callbacks)No
+ +

代码示例

+ +

通过XMLHttpRequest API加载资源的示例(run it live,并查看see the source):

+ +
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);
+ +

缺陷

+ + + +

浏览器兼容性

+ +

非常好的一般支持,尽管API中回调的确切支持取决于特定的API。有关更具体的支持信息,请参阅您正在使用的API的参考文档。

+ +

更多信息

+ + + +

setTimeout()

+ +

setTimeout() 是一种允许您在经过任意时间后运行函数的方法

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
YesYes (recursive timeouts)Yes (nested timeouts)No
+ +

代码示例

+ +

这里浏览器将在执行匿名函数之前等待两秒钟,然后将显示警报消息(see it running livesee the source code):

+ +
let myGreeting = setTimeout(function() {
+  alert('Hello, Mr. Universe!');
+}, 2000)
+ +

缺陷

+ +

您可以使用递归的setTimeout()调用以类似于setInterval()的方式重复运行函数,使用如下代码:

+ +
let i = 1;
+setTimeout(function run() {
+  console.log(i);
+  i++;
+
+  setTimeout(run, 100);
+}, 100);
+ +

递归setTimeout()setInterval()之间存在差异:

+ + + +

当你的代码有可能比你分配的时间间隔更长时间运行时,最好使用递归的setTimeout() ––这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。

+ +

浏览器兼容性

+ +

{{Compat("api.WindowOrWorkerGlobalScope.setTimeout")}}

+ +

更多信息

+ + + +

setInterval()

+ +

setInterval()函数允许重复执行一个函数,并设置时间间隔。不如requestAnimationFrame()有效率,但允许您选择运行速率/帧速率。

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoYesNo (unless it is the same one)No
+ +

代码示例

+ +

以下函数创建一个新的Date()对象,使用toLocaleTimeString()从中提取时间字符串,然后在UI中显示它。然后我们使用setInterval()每秒运行一次,创建每秒更新一次的数字时钟的效果(see this livesee the source):

+ +
function displayTime() {
+   let date = new Date();
+   let time = date.toLocaleTimeString();
+   document.getElementById('demo').textContent = time;
+}
+
+const createClock = setInterval(displayTime, 1000);
+ +

缺陷

+ + + +

浏览器兼容性

+ +

{{Compat("api.WindowOrWorkerGlobalScope.setInterval")}}

+ +

更多信息

+ + + +

requestAnimationFrame()

+ +

requestAnimationFrame()是一种允许您以给定当前浏览器/系统的最佳帧速率重复且高效地运行函数的方法。除非您需要特定的速率帧,否则您应该尽可能使用它而不要去使用setInterval()/recursive setTimeout()

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoYesNo (unless it is the same one)No
+ +

代码示例

+ +

一个简单的动画旋转器;你可以查看example live on GitHub(参见 source code ):

+ +
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();
+ +

缺陷

+ + + +

浏览器兼容性

+ +

{{Compat("api.Window.requestAnimationFrame")}}

+ +

更多信息

+ + + +

Promises

+ +

Promises 是一种JavaScript功能,允许您运行异步操作并等到它完全完成后再根据其结果运行另一个操作。 Promise是现代异步JavaScript的支柱。

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoNoYesSee Promise.all(), below
+ +

代码示例

+ +

以下代码从服务器获取图像并将其显示在 {{htmlelement("img")}} 元素中;(see it live alsothe source code):

+ +
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);
+});
+ +

缺陷

+ +

Promise链可能很复杂,难以解析。如果你嵌套了许多promises,你最终可能会遇到类似的麻烦来回调地狱。例如:

+ +
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...
+ +

最好使用promises的链功能,这样使用更平顺,更易于解析的结构:

+ +
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);
+});
+ +

乃至:

+ +
remotedb.allDocs(...)
+.then(resultOfAllDocs => {
+  return localdb.put(...);
+})
+.then(resultOfPut => {
+  return localdb.get(...);
+})
+.then(resultOfGet => {
+  return localdb.put(...);
+})
+.catch(err => console.log(err));
+ +

这涵盖了很多基础知识。对于更完整的论述,请参阅诺兰劳森的We have a problem with promises

+ +

浏览器兼容性

+ +

{{Compat("javascript.builtins.Promise")}}

+ +

更多信息

+ + + +

Promise.all()

+ +

一种JavaScript功能,允许您等待多个promises完成,然后根据所有其他promises的结果运行进一步的操作。

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoNoNoYes
+ +

代码示例

+ +

以下示例从服务器获取多个资源,并使用Promise.all()等待所有资源可用,然后显示所有这些资源––  see it live,并查看source code

+ +
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);
+});
+ +

缺陷

+ + + +

浏览器兼容性

+ +

{{Compat("javascript.builtins.Promise.all")}}

+ +

更多信息

+ + + +

Async/await

+ +

构造在promises之上的语法糖,允许您使用更像编写同步回调代码的语法来运行异步操作。

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoNoYesYes (in combination with Promise.all())
+ +

代码示例

+ +

以下示例是我们之前看到的简单承诺示例的重构,该示例获取并显示图像,使用async / await编写(see it live,并查看source code):

+ +
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();
+ +

缺陷

+ + + +

浏览器兼容性

+ +

{{Compat("javascript.statements.async_function")}}

+ +

更多信息

+ + + +

{{PreviousMenu("Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}

+ +

本章内容

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/concepts/index.html b/files/zh-cn/learn/javascript/asynchronous/concepts/index.html new file mode 100644 index 0000000000..6e59cda54b --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/concepts/index.html @@ -0,0 +1,162 @@ +--- +title: 通用异步编程概念 +slug: learn/JavaScript/异步/概念 +tags: + - JavaScript + - Promises + - Threads + - 学习 + - 异步 + - 阻塞 +translation_of: Learn/JavaScript/Asynchronous/Concepts +--- +
{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}
+ +

在本文中,我们将介绍与异步编程相关的一些重要概念,以及它们在浏览器和JavaScript里的体现。在学习本系列的其他文章之前,你应该先理解这些概念。

+ + + + + + + + + + + + +
预备条件:拥有基本的计算机知识,对JavaScript原理有一定了解。
目标:理解异步编程的基本概念,以及异步编程在浏览器和JavaScript里面的表现。
+ +

异步?

+ +

通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕.

+ +

Mac 用户有时会经历过这种旋转的彩虹光标(常称为沙滩球),操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。

+ +

multi-colored macos beachball busy spinner

+ +

这是令人沮丧的体验,没有充分利用计算机的计算能力 — 尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,同时计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是异步编程的出发点。你正在使用的编程环境(就web开发而言,编程环境就是web浏览器)负责为你提供异步运行此类任务的API。

+ +

产生阻塞的代码

+ +

异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做阻塞;这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。

+ +

我们来看一些阻塞的例子。

+ +

例子: simple-sync.html  (see it running live), 在按钮上添加了一个事件监听器,当按钮被点击,它就开始运行一个非常耗时的任务(计算1千万个日期,并在console里显示最后一个日期),然后在DOM里面添加一个段落:

+ +
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);
+});
+ +

运行这个例子的时候,打开JavaScript console,然后点击按钮 — 你会注意到,直到日期的运算结束,最后一个日期在console上显示出来,段落才会出现在网页上。代码按照源代码的顺序执行,只有前面的代码结束运行,后面的代码才会执行。

+ +
+

Note: 这个例子不现实:在实际情况中一般不会发生,没有谁会计算1千万次日期,它仅仅提供一个非常直观的体验.

+
+ +

第二个例子, simple-sync-ui-blocking.html (see it live), 我们模拟一个在现实的网页可能遇到的情况:因为渲染UI而阻塞用户的互动,这个例子有2个按钮:

+ + + +
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!')
+);
+ +

如果你点击第一个按钮,然后快速点击第二个,会注意到alert消息并没有出现,只有等到圆圈都画完以后,才会出现:因为第一个操作没有完成之前阻塞了第二个操作的运行.

+ +
+

Note: 当然,这个例子也很丑陋,因为我们只是在模拟阻塞效果。但在现实中,这是一个很常见的问题。开发人员们一直在努力缓解它。

+
+ +

为什么是这样? 答案是:JavaScript一般来说是单线程的(single threaded接着我们来介绍线程的概念。

+ +

线程

+ +

一个线程是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:

+ +
Task A --> Task B --> Task C
+ +

每个任务顺序执行,只有前面的结束了,后面的才能开始。

+ +

正如我们之前所说,现在的计算机大都有多个内核(core),因此可以同时执行多个任务。支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:

+ +
Thread 1: Task A --> Task B
+Thread 2: Task C --> Task D
+ +

JavaScript 是单线程的

+ +

JavaScript 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(main thread)。我们上面的例子运行如下:

+ +
Main thread: Render circles to canvas --> Display alert()
+ +

经过一段时间,JavaScript获得了一些工具来帮助解决这种问题。通过 Web workers 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)

+ +
Main thread: Task A --> Task C
+Worker thread: Expensive task B
+ +

记住这些,请查看simple-sync-worker.html (see it running live) , 再次打开浏览器的JavaScript 控制台。这个例子重写了前例:在一个单独的worker线程中计算一千万次日期,你再点击按钮,现在浏览器可以在日期计算完成之前显示段落,阻塞消失了。

+ +

异步代码

+ +

web workers相当有用,但是他们确实也有局限。主要的一个问题是他们不能访问 {{Glossary("DOM")}} — 不能让一个worker直接更新UI。我们不能在worker里面渲染1百万个蓝色圆圈,它基本上只能做算数的苦活。

+ +

其次,虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。考虑下面的情况:

+ +
Main thread: Task A --> Task B
+ +

在这种情况下,比如说Task A 正在从服务器上获取一个图片之类的资源,Task B 准备在图片上加一个滤镜。如果开始运行Task A 后立即尝试运行Task B,你将会得到一个错误,因为图像还没有获取到。

+ +
Main thread: Task A --> Task B --> |Task D|
+Worker thread: Task C -----------> |      |
+ +

在这种情况下,假设Task D 要同时使用 Task B 和Task C的结果,如果我们能保证这两个结果同时提供,程序可能正常运行,但是这不太可能。如果Task D 尝试在其中一个结果尚未可用的情况下就运行,程序就会抛出一个错误。

+ +

为了解决这些问题,浏览器允许我们异步运行某些操作。像Promises 这样的功能就允许让一些操作运行 (比如:从服务器上获取图片),然后等待直到结果返回,再运行其他的操作:

+ +
Main thread: Task A                   Task B
+    Promise:      |__async operation__|
+ +

由于操作发生在其他地方,因此在处理异步操作的时候,主线程不会被阻塞。

+ +

我们将在下一篇文章中开始研究如何编写异步代码。 非常令人兴奋,对吧? 继续阅读!

+ +

总结

+ +

围绕异步编程领域,现代软件设计正在加速旋转,就为了让程序在一个时间内做更多的事情。当你使用更新更强大的API时,你会发现在更多的情况下,使用异步编程是唯一的途径。以前写异步代码很困难,现在也需要你来适应,但是已经变容易了很多。在余下的部分,我们将进一步探讨异步代码的重要性,以及如何设计代码来防止前面已经提到过的问题。

+ +

模块大纲

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/index.html b/files/zh-cn/learn/javascript/asynchronous/index.html new file mode 100644 index 0000000000..3d89f5a060 --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/index.html @@ -0,0 +1,59 @@ +--- +title: 异步JavaScript +slug: learn/JavaScript/异步 +tags: + - JavaScript + - Promises + - requestAnimationFrame + - 初学者 + - 回调函数 + - 异步 + - 指南 + - 等待 + - 脚本编程 + - 设置定时器 + - 设置间隔 +translation_of: Learn/JavaScript/Asynchronous +--- +
+ +
{{LearnSidebar}}
+ +
+ +

在这个模块,我们将查看 {{Glossary("asynchronous")}} {{Glossary("JavaScript")}},异步为什么很重要,以及怎样使用异步来有效处理潜在的阻塞操作,比如从服务器上获取资源。

+ +

预备知识

+ +

异步JavaScript 是一个相当高级的话题,建议你先完成( JavaScript first stepsJavaScript building blocks) 两个模块的学习后再来学习。

+ +

如果你还不熟悉异步编程的概念,请从 通用异步编程概念开始. 如果熟悉的话,可以直接从介绍异步JavaScript 开始.

+ +
+

Note: 如果在当前阅读本文档而使用的电子设备(电脑/平板/其他)上,你没有权限生成自己的文件,你可以使用 JSBin or Thimble 在线编程工具来尝试文章里面的代码示例

+
+ +

指南

+ +
+
一般异步编程概念
+
+

浏览 异步相关的重要概念,在浏览器和JS里面的应用,学习本模块其他文章之前,你应该理解这些基本的概念。

+
+
介绍异步JS
+
这篇文章简单概括同步JS遇到的问题,首次提到一些不同的异步JS技术,他们是如何解决同步JS的问题
+
合作异步JS:Timeouts and intervals
+
在这里介绍JS传统的异步方法:在一段时间后运行 或者 在设定时间周期反复运行,看看这些技术如何使用,有什么内在的问题.
+
优雅的处理异步操作:Promises
+
Promises 是JS一个相对比较新的特性,你可以使用它来延迟一些操作直到前面的代码已经返回结果。对于时间上顺序完成的一系列操作,这个真的有用。本文展示promises 如何工作,使用WebAPIs何处会见到它, 最重要的:怎样写你自己的promises.
+
让异步编程简单: async and await
+
Promises 有点复杂, 所以现代的浏览器都实现了 async 函数和 await 操作符 —--前者允许标准函数隐式地和 promises 工作, 后者可以在async 函数里面使用,等待promises运行结束,函数再继续运行。
+
选择正确的方法
+
结束本模块之前,回顾一下已经讨论的编程技术和特性:什么时候用哪个。有推荐,也有常见的陷阱提醒。
+
+ +

参考

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/introducing/index.html b/files/zh-cn/learn/javascript/asynchronous/introducing/index.html new file mode 100644 index 0000000000..1792c0e086 --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/introducing/index.html @@ -0,0 +1,272 @@ +--- +title: 异步JavaScript简介 +slug: learn/JavaScript/异步/简介 +translation_of: Learn/JavaScript/Asynchronous/Introducing +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/异步/概念", "Learn/JavaScript/异步/Timeouts_and_intervals", "Learn/JavaScript/异步")}}
+ +

在本文中,我们简要回顾一下与同步JavaScript相关的问题,首次介绍你将遇到的一些不同的异步技术,并展示如何使用这些技术解决问题。

+ + + + + + + + + + + + +
预备条件:基本的计算机素养,以及对JavaScript 基础知识的较好的理解。
目标:熟悉什么是异步JavaScript,与同步JavaScript 的区别,以及使用场合。
+ +

同步JavaScript

+ +

要理解什么是{{Glossary("异步")}} JavaScript ,我们应该从确切理解{{Glossary("同步")}} JavaScript 开始。本节回顾我们在上一篇文章中看到的一些信息。

+ +

前面学的很多知识基本上都是同步的 — 运行代码,然后浏览器尽快返回结果。先看一个简单的例子 (运行它, 这是源码):

+ +
const btn = document.querySelector('button');
+btn.addEventListener('click', () => {
+  alert('You clicked me!');
+
+  let pElem = document.createElement('p');
+  pElem.textContent = 'This is a newly-added paragraph.';
+  document.body.appendChild(pElem);
+});
+
+ +

这段代码, 一行一行的顺序执行:

+ +
    +
  1. 先取得一个在DOM里面的 {{htmlelement("button")}} 引用。
  2. +
  3. 点击按钮的时候,添加一个 click 事件监听器: +
      +
    1. alert() 消息出现。
    2. +
    3. 一旦alert 结束,创建一个{{htmlelement("p")}} 元素。
    4. +
    5. 给它的文本内容赋值。
    6. +
    7. 最后,把这个段落放进网页。
    8. +
    +
  4. +
+ +

每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停. 因为前篇文章提到过 JavaScript is single threaded. 任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。

+ +

所以上面的例子,点击了按钮以后,段落不会创建,直到在alert消息框中点击ok,段落才会出现,你可以自己试试:

+ + + +

{{EmbedLiveSample('Synchronous_JavaScript', '100%', '70px')}}

+ +
+

Note: 这很重要请记住,alert()在演示阻塞效果的时候非常有用,但是在正式代码里面,它就是一个噩梦。

+
+ +

异步JavaScript

+ +

就前面提到的种种原因(比如,和阻塞相关)很多网页API特性使用异步代码,特别是从外部的设备上获取资源,譬如,从网络获取文件,访问数据库,从网络摄像头获得视频流,或者向VR头罩广播图像。

+ +

为什么使用异步代码这么难?看一个例子,当你从服务器获取一个图像,通常你不可能立马就得到,这需要时间,虽然现在的网络很快。这意味着下面的伪代码可能不能正常工作:

+ +
var response = fetch('myImage.png');
+var blob = response.blob();
+// display your image blob in the UI somehow
+ +

因为你不知道下载图片会多久,所以第二行代码执行的时候可能报错(可能间歇的,也可能每次)因为图像还没有就绪。取代的方法就是,代码必须等到 response 返回才能继续往下执行。

+ +

在JavaScript代码中,你经常会遇到两种异步编程风格:老派callbacks,新派promise。下面就来分别介绍。

+ +

异步callbacks

+ +

异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成,或者其他有趣的事情发生了。使用callbacks 有一点老套,在一些老派但经常使用的API里面,你会经常看到这种风格。

+ +

举个例子,异步callback 就是{{domxref("EventTarget.addEventListener", "addEventListener()")}}第二个参数(前面的例子):

+ +
btn.addEventListener('click', () => {
+  alert('You clicked me!');
+
+  let pElem = document.createElement('p');
+  pElem.textContent = 'This is a newly-added paragraph.';
+  document.body.appendChild(pElem);
+});
+ +

第一个参数是侦听的事件类型,第二个就是事件发生时调用的回调函数。.

+ +

当我们把回调函数作为一个参数传递给另一个函数时,仅仅是把回调函数定义作为参数传递过去 — 回调函数并没有立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数。

+ +

你可以自己写一个容易的,包含回调函数的函数。来看另外一个例子,用 XMLHttpRequest API (运行它, 源代码) 加载资源:

+ +
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);
+ +

创建 displayImage() 函数,简单的把blob传递给它,生成objectURL,然后再生成一个image元素,把objectURL作为image的源地址,最后显示这张图片。  然后,我们创建 loadAsset() 函数,把URL,type,和回调函数同时都作为参数。函数用 XMLHttpRequest (通常缩写 "XHR") 获取给定URL的资源,在获得资源响应后再把响应作为参数传递给回调函数去处理。 (使用 onload 事件处理) ,有点烧脑,是不是?!

+ +

回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数,所以对下载好的资源,你可以采用不同的操作来处理,譬如 processJSON(), displayText(), 等等。

+ +

请注意,不是所有的回调函数都是异步的 — 有一些是同步的。一个例子就是使用 {{jsxref("Array.prototype.forEach()")}} 来遍历数组 (运行, 源代码):

+ +
const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];
+
+gods.forEach(function (eachName, index){
+  console.log(index + '. ' + eachName);
+});
+ +

在这个例子中,我们遍历一个希腊神的数组,并在控制台中打印索引和值。forEach() 需要的参数是一个回调函数,回调函数本身带有两个参数,数组元素和索引值。它无需等待任何事情,立即运行。

+ +

Promises

+ +

Promises 是新派的异步代码,现代的web APIs经常用到。 fetch() API就是一个很好的例子, 它基本上就是一个现代版的,更高效的 {{domxref("XMLHttpRequest")}}。看个例子,来自于文章 Fetching data from the server

+ +
fetch('products.json').then(function(response) {
+  return response.json();
+}).then(function(json) {
+  products = json;
+  initialize();
+}).catch(function(err) {
+  console.log('Fetch problem: ' + err.message);
+});
+ +
+

Note: 在GitHub上有完整的代码 (see the source here, and also see it running live)。

+
+ +

这里fetch() 只需要一个参数— 资源的网络 URL — 返回一个 promise. promise 是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是浏览器说“我保证尽快给您答复”的方式,因此得名“promise”。

+ +

这个概念需要练习来适应;它感觉有点像运行中的{{interwiki("wikipedia", "薛定谔猫")}}。这两种可能的结果都还没有发生,因此fetch操作目前正在等待浏览器试图在将来某个时候完成该操作的结果。然后我们有三个代码块链接到fetch()的末尾:

+ + + +
+

Note: 在本模块稍后的部分中,你将学习更多关于promise的内容,所以如果你还没有完全理解这些promise,请不要担心。

+
+ +

事件队列

+ +

像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成,然后将结果返回到JavaScript环境。

+ +

Promises 对比 callbacks

+ +

promises与旧式callbacks有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。

+ +

然而,Promise是专门为异步操作而设计的,与旧式回调相比具有许多优点:

+ + + +

异步代码的本质

+ +

让我们研究一个示例,它进一步说明了异步代码的本质,展示了当我们不完全了解代码执行顺序以及将异步代码视为同步代码时可能发生的问题。下面的示例与我们之前看到的非常相似( 运行它 和 源代码)。一个不同之处在于,我们包含了许多{{domxref("console.log()")}}语句,以展示代码将在其中执行的顺序。

+ +
console.log ('Starting');
+let image;
+
+fetch('coffee.jpg').then((response) => {
+  console.log('It worked :)')
+  return response.blob();
+}).then((myBlob) => {
+  let objectURL = URL.createObjectURL(myBlob);
+  image = document.createElement('img');
+  image.src = objectURL;
+  document.body.appendChild(image);
+}).catch((error) => {
+  console.log('There has been a problem with your fetch operation: ' + error.message);
+});
+
+console.log ('All done!');
+ +

浏览器将会执行代码,看见第一个console.log() 输出(Starting) ,然后创建image 变量。

+ +

然后,它将移动到下一行并开始执行fetch()块,但是,因为fetch()是异步执行的,没有阻塞,所以在promise相关代码之后程序继续执行,从而到达最后的console.log()语句(All done!)并将其输出到控制台。

+ +

只有当fetch() 块完成运行返回结果给.then() ,我们才最后看到第二个console.log() 消息 (It worked ;)) . 所以 这些消息 可能以 和你预期不同的顺序出现:

+ + + +

如果你感到疑惑,考虑下面这个小例子:

+ +
console.log("registering click handler");
+
+button.addEventListener('click', () => {
+  console.log("get click");
+});
+
+console.log("all done");
+ +

这在行为上非常相似——第一个和第三个console.log()消息将立即显示,但是第二个消息将被阻塞,直到有人单击鼠标按钮。前面的示例以相同的方式工作,只是在这种情况下,第二个消息在promise链上被阻塞,直到获取资源后再显示在屏幕上,而不是单击。

+ +

要查看实际情况,请尝试获取示例的本地副本,并将第三个console.log()调用更改为以下命令:

+ +
console.log ('All done! ' + image.src + 'displayed.');
+ +

此时控制台将会报错,而不会显示第三个 console.log 的信息:

+ +
TypeError: image is undefined; can't access its "src" property
+ +

这是因为:浏览器运行第三个console.log()的时候,fetch() 语句块还没有完成,因此image还没有赋值。

+ +

主动学习:把代码全部异步化

+ +

要修复有问题的fetch()示例并使三个console.log()语句按期望的顺序出现,还可以让第三个console.log()语句异步运行。这可以通过将它移动到另一个then()块中来实现,然后将它链接到第二个then()块的末尾,或者简单地将它移动到第二个then()块中。现在就试试。

+ +
+

Note: 如果你困住了,你可以在这里找到答案 (这里运行)。在后面的文章:用Promises优雅的异步编程, 你将会发现更多信息。

+
+ +

小结

+ +

在最基本的形式中,JavaScript是一种同步的、阻塞的、单线程的语言,在这种语言中,一次只能执行一个操作。但web浏览器定义了函数和API,允许我们当某些事件发生时不是按照同步方式,而是异步地调用函数(比如,时间的推移,用户通过鼠标的交互,或者获取网络数据)。这意味着您的代码可以同时做几件事情,而不需要停止或阻塞主线程。

+ +

异步还是同步执行代码,取决于我们要做什么。

+ +

有些时候,我们希望事情能够立即加载并发生。例如,当将一些用户定义的样式应用到一个页面时,您希望这些样式能够尽快被应用。

+ +

但是,如果我们正在运行一个需要时间的操作,比如查询数据库并使用结果填充模板,那么最好将该操作从主线程中移开使用异步完成任务。随着时间的推移,您将了解何时选择异步技术比选择同步技术更有意义。

+ + + +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Concepts", "Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous")}}

+ +

模块大纲

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/promises/index.html b/files/zh-cn/learn/javascript/asynchronous/promises/index.html new file mode 100644 index 0000000000..665bda8129 --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/promises/index.html @@ -0,0 +1,599 @@ +--- +title: 优雅的异步处理 +slug: learn/JavaScript/异步/Promises语法 +translation_of: Learn/JavaScript/Asynchronous/Promises +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}
+ +

Promise 是 JavaScript 语言的一个相对较新的功能,允许你推迟进一步的操作,直到上一个操作完成或响应其失败。这对于设置一系列异步操作以正常工作非常有用。本文向你展示了promises如何工作,如何在Web API中使用它们以及如何编写自己的API

+ + + + + + + + + + + + +
前提条件:基本的计算机素养,具备基础的JavaScript知识
目标:理解并使用学习如何使用Promises
+ +

什么是promises?

+ +

我们在教程的第一篇文章中简要地了解了 Promises,接下来我们将在更深层次理解Promise。

+ +

本质上,Promise 是一个对象,代表操作的中间状态 —— 正如它的单词含义 '承诺' ,它保证在未来可能返回某种结果。虽然 Promise 并不保证操作在何时完成并返回结果,但是它保证当结果可用时,你的代码能正确处理结果,当结果不可用时,你的代码同样会被执行,来优雅的处理错误。

+ +

通常你不会对一个异步操作从开始执行到返回结果所用的时间感兴趣(除非它耗时过长),你会更想在任何时候都能响应操作结果,当然它不会阻塞其余代码的执行就更好了。

+ +

你与 Promise 常见的交互之一就是 Web API 返回的 promise 对象。让我们设想一个视频聊天应用程序,该程序有一个展示用户的朋友列表的窗口,可以点击朋友旁边的按钮对朋友视频呼叫。

+ +

该按钮的处理程序调用 {{domxref("MediaDevices.getUserMedia", "getUserMedia()")}} 来访问用户的摄像头和麦克风。由于 getUserMedia() 必须确保用户具有使用这些设备的权限,并询问用户要使用哪个麦克风和摄像头(或者是否仅进行语音通话,以及其他可能的选项),因此它会产生阻塞,直到用户做出所有的决定,并且摄像头和麦克风都已启用。此外,用户可能不会立即响应权限请求。所以 getUserMedia() 可能需要很长时间。

+ +

由于 getUserMedia() 是在浏览器的主线程进行调用,整个浏览器将会处于阻塞状态直到 getUserMedia() 返回,这是不应该发生的;不使用Promise,浏览器将处于不可用状态直到用户为摄像头和麦克风做出决定。因此 getUserMedia() 返回一个Promise对象,即 {{jsxref("promise")}},一旦  {{domxref("MediaStream")}} 流可用才去解析,而不是等待用户操作、启动选中的设备并直接返回从所选资源创建的 {{domxref("MediaStream")}} 流。

+ +

上述视频聊天应用程序的代码可能像下面这样:

+ +
function handleCallButton(evt) {
+  setStatusMessage("Calling...");
+  navigator.mediaDevices.getUserMedia({video: true, audio: true})
+    .then(chatStream => {
+      selfViewElem.srcObject = chatStream;
+      chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream));
+      setStatusMessage("Connected");
+    }).catch(err => {
+      setStatusMessage("Failed to connect");
+    });
+}
+
+ +

这个函数在开头调用 setStatusMessage() 来更新状态显示信息"Calling...", 表示正在尝试通话。接下来调用 getUserMedia(),请求具有视频及音频轨的流,一旦获得这个流,就将其显示在"selfViewElem"的video元素中。接下来将这个流的每个轨道添加到表示与另一个用户的连接的 WebRTC,参见{{domxref("RTCPeerConnection")}}。在这之后,状态显示为"Connected"。

+ +

如果getUserMedia()失败,则catch块运行。这使用setStatusMessage()更新状态框以指示发生错误。

+ +

这里重要的是getUserMedia()调用几乎立即返回,即使尚未获得相机流。即使handleCallButton()函数向调用它的代码返回结果,当getUserMedia()完成工作时,它也会调用你提供的处理程序。只要应用程序不假设流式传输已经开始,它就可以继续运行。

+ +
+

注意:  如果你有兴趣,可以在文章Signaling and video calling中了解有关此高级主题的更多信息。在该示例中使用与此类似的代码,但更完整。

+
+ +

 回调函数的麻烦

+ +

要完全理解为什么 promise 是一件好事,应该回想之前的回调函数,理解它们造成的困难。

+ +

我们来谈谈订购披萨作为类比。为了使你的订单成功,你必须按顺序执行,不按顺序执行或上一步没完成就执行下一步是不会成功的:

+ +
    +
  1. 选择配料。如果你是优柔寡断,这可能需要一段时间,如果你无法下定决心或者决定换咖喱,可能会失败。
  2. +
  3. 下订单。返回比萨饼可能需要一段时间,如果餐厅没有烹饪所需的配料,可能会失败。
  4. +
  5. 然后你收集你的披萨吃。如果你忘记了自己的钱包,那么这可能会失败,所以无法支付比萨饼的费用!
  6. +
+ +

对于旧式callbacks,上述功能的伪代码表示可能如下所示:

+ +
chooseToppings(function(toppings) {
+  placeOrder(toppings, function(order) {
+    collectOrder(order, function(pizza) {
+      eatPizza(pizza);
+    }, failureCallback);
+  }, failureCallback);
+}, failureCallback);
+ +

这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用failureCallback()(每个嵌套函数一次),还有其他问题。

+ +

使用promise改良

+ +

Promises使得上面的情况更容易编写,解析和运行。如果我们使用异步promises代表上面的伪代码,我们最终会得到这样的结果:

+ +
chooseToppings()
+.then(function(toppings) {
+  return placeOrder(toppings);
+})
+.then(function(order) {
+  return collectOrder(order);
+})
+.then(function(pizza) {
+  eatPizza(pizza);
+})
+.catch(failureCallback);
+ +

这要好得多 - 更容易看到发生了什么,我们只需要一个.catch()块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.then()块返回一个新的promise,当.then()块运行完毕时它会解析。聪明,对吗?

+ +

使用箭头函数,你可以进一步简化代码:

+ +
chooseToppings()
+.then(toppings =>
+  placeOrder(toppings)
+)
+.then(order =>
+  collectOrder(order)
+)
+.then(pizza =>
+  eatPizza(pizza)
+)
+.catch(failureCallback);
+ +

甚至这样:

+ +
chooseToppings()
+.then(toppings => placeOrder(toppings))
+.then(order => collectOrder(order))
+.then(pizza => eatPizza(pizza))
+.catch(failureCallback);
+ +

这是有效的,因为使用箭头函数 () => x()=> {return x;}  的有效简写; 。

+ +

你甚至可以这样做,因为函数只是直接传递它们的参数,所以不需要额外的函数层:

+ +
chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);
+ +

但是,这并不容易阅读,如果你的块比我们在此处显示的更复杂,则此语法可能无法使用。

+ +
+

注意: 你可以使用 async/await 语法进行进一步的改进,我们将在下一篇文章中深入讨论。

+
+ +

最基本的,promise与事件监听器类似,但有一些差异:

+ + + +

 解释promise的基本语法:一个真实的例子

+ +

Promises 很重要,因为大多数现代Web API都将它们用于执行潜在冗长任务的函数。要使用现代Web技术,你需要使用promises。在本章的后面我们将看看如何编写自己的promises,但是现在我们将看一些你将在Web API中遇到的简单示例。

+ +

在第一个示例中,我们将使用fetch()方法从Web获取图像,blob() 方法来转换获取响应的原始内容到 Blob 对象,然后在 <img> 元素内显示该blob。这与我们在 first article of the series中看到的示例非常相似,但是会在构建你自己的基于 promise 的代码时有所不同。 

+ +
+

注意: 下列代码无法直接运行(i.e. via a file:// URL)。你需要本地测试服务器,或是 GlitchGitHub pages 这样的在线解决方案。

+
+ +
    +
  1. +

    首先,下载我们的 simple HTML templatesample image file

    +
  2. +
  3. +

    将 {{htmlelement("script")}} 元素添加在HTML {{htmlelement("body")}} 的底部。

    +
  4. +
  5. +

    在 {{HTMLElement("script")}} 元素内,添加以下行:

    + +
    let promise = fetch('coffee.jpg');
    + +

    这会调用 fetch() 方法,将图像的URL作为参数从网络中提取。这也可以将options对象作为可选的第二个参数,但我们现在只使用最简单的版本。我们将 fetch() 返回的promise对象存储在一个名为promise的变量中。正如我们之前所说的,这个对象代表了一个最初既不成功也不失败的中间状态 - 这个状态下的promise的官方术语叫作pending

    +
  6. +
  7. 为了响应成功完成操作(在这种情况下,当返回{{domxref("Response")}}时),我们调用promise对象的.then()方法。 .then()块中的回调(称为执行程序)仅在promise调用成功完成时运行并返回{{domxref("Response")}}对象 - 在promise-speak中,当它已被满足时。它将返回的{{domxref("Response")}}对象作为参数传递。
  8. +
+ +
+

注意: .then()块的工作方式类似于使用AddEventListener()向对象添加事件侦听器时的方式。它不会在事件发生之前运行(当promise履行时)。最显着的区别是.then()每次使用时只运行一次,而事件监听器可以多次调用。

+
+ +

我们立即对此响应运行blob()方法以确保响应主体完全下载,并且当它可用时将其转换为我们可以执行某些操作的Blob对象。返回的结果如下:

+ +
response => response.blob()
+ +

这是下面的简写

+ +
function(response) {
+    return response.blob();
+}
+
+ +

好的,我们还需要做点额外的工作。Fetch promises 不会产生 404 或 500错误,只有在产生像网路故障的情况时才会不工作。总的来说,Fetch promises 总是成功运行,即使response.ok 属性是 false。为了产生404错误,我们需要判断 response.ok ,如果是 false,抛出错误,否则返回 blob。就像下面的代码这样做。

+ +
let promise2 = promise.then(response => {
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    return response.blob();
+  }
+});
+ +

5. 每次调用.then()都会创建一个新的promise。这非常有用;因为blob()方法也返回一个promise,我们可以通过调用第二个promise的.then()方法来处理它在履行时返回的Blob对象。因为我们想要对blob执行一些更复杂的操作,而不仅仅运行单个方法并返回结果,这次我们需要将函数体包装成花括号(否则会抛出错误)。

+ +

将以下内容添加到代码的末尾:

+ +
let promise3 = promise2.then(myBlob => {})
+ +

6. 现在让我们填写执行程序函数的主体。在花括号内添加以下行:

+ +
let objectURL = URL.createObjectURL(myBlob);
+let image = document.createElement('img');
+image.src = objectURL;
+document.body.appendChild(image);
+ +

这里我们运行{{domxref("URL.createObjectURL()")}}方法,将其作为Blob在第二个promise实现时返回的参数传递。这将返回指向该对象的URL。然后我们创建一个{{htmlelement("img")}}元素,将其src属性设置为等于对象URL并将其附加到DOM,这样图像就会显示在页面上!

+ +

如果你保存刚刚创建的HTML文件并将其加载到浏览器中,你将看到图像按预期显示在页面中。干得好!

+ +
+

注意: 你可能会注意到这些例子有点做作。你可以取消整个fetch()blob()链,只需创建一个<img>元素并将其src属性值设置为图像文件的URL,即coffee.jpg。然而,我们选择了这个例子,因为它以简单的方式展示了promise,而不是真实世界的适当性。

+
+ +

响应失败

+ +

缺少一些东西 - 如果其中一个promise失败(rejects,in promise-speak),目前没有什么可以明确地处理错误。我们可以通过运行前一个promise的 .catch() 方法来添加错误处理。立即添加:

+ +
let errorCase = promise3.catch(e => {
+  console.log('There has been a problem with your fetch operation: ' + e.message);
+});
+ +

要查看此操作,请尝试拼错图像的URL并重新加载页面。该错误将在浏览器的开发人员工具的控制台中报告。

+ +

如果你根本不操心包括的 .catch() 块,这并没有做太多的事情,但考虑一下(指.catch()块) ––这会使我们可以完全控制错误处理方式。在真实的应用程序中,你的.catch()块可以重试获取图像,或显示默认图像,或提示用户提供不同的图像URL等等。

+ +
+

注意: 你可以参考 our version of the example live (参阅 source code ).

+
+ +

将代码块链在一起

+ +

这是写出来的一种非常简便的方式;我们故意这样做是为了帮助你清楚地了解发生了什么。如本文前面所示,你可以将.then()块(以及.catch()块)链接在一起。上面的代码也可以这样写(参阅GitHub上的simple-fetch-chained.html ):

+ +
fetch('coffee.jpg')
+.then(response => {
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    return 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);
+});
+ +

请记住,履行的promise所返回的值将成为传递给下一个 .then() 块的executor函数的参数。

+ +
+

注意: promise中的.then()/catch()块基本上是同步代码中try...catch块的异步等价物。请记住,同步try ... catch在异步代码中不起作用。

+
+ +

Promise术语回顾

+ +

在上面的部分中有很多要介绍的内容,所以让我们快速回过头来给你一个简短的指南,你可以将它添加到书签中,以便将来更新你的记忆。你还应该再次阅读上述部分,以确保这些概念坚持下去。

+ +
    +
  1. 创建promise时,它既不是成功也不是失败状态。这个状态叫作pending(待定)。
  2. +
  3. 当promise返回时,称为 resolved(已解决). +
      +
    1. 一个成功resolved的promise称为fullfilled实现)。它返回一个值,可以通过将.then()块链接到promise链的末尾来访问该值。 .then()块中的执行程序函数将包含promise的返回值。
    2. +
    3. 一个不成功resolved的promise被称为rejected拒绝)了。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将.catch()块链接到promise链的末尾来访问此原因。
    4. +
    +
  4. +
+ +

 运行代码以响应多个Promises的实现

+ +

上面的例子向我们展示了使用promises的一些真正的基础知识。现在让我们看一些更高级的功能。首先,链接进程一个接一个地发生都很好,但是如果你想在一大堆Promises全部完成之后运行一些代码呢?

+ +

 你可以使用巧妙命名的Promise.all()静态方法完成此操作。这将一个promises数组作为输入参数,并返回一个新的Promise对象,只有当数组中的所有promise都满足时才会满足。它看起来像这样:

+ +
Promise.all([a, b, c]).then(values => {
+  ...
+});
+ +

如果它们都实现,那么数组中的结果将作为参数传递给.then()块中的执行器函数。如果传递给Promise.all()的任何一个 promise 拒绝,整个块将拒绝

+ +

这非常有用。想象一下,我们正在获取信息以在内容上动态填充页面上的UI功能。在许多情况下,接收所有数据然后才显示完整内容,而不是显示部分信息。

+ +

让我们构建另一个示例来展示这一点。

+ +
    +
  1. +

    下载我们页面模板(page template)的新副本,并再次在结束</ body>标记之前放置一个<script>元素。

    +
  2. +
  3. +

    下载我们的源文件(coffee.jpg, tea.jpg和 description.txt),或者随意替换成你自己的文件。

    +
  4. +
  5. +

    在我们的脚本中,我们将首先定义一个函数,该函数返回我们要发送给Promise.all()的promise。如果我们只想运行Promise.all()块以响应三个fetch()操作完成,这将很容易。我们可以这样做:

    + +
    let a = fetch(url1);
    +let b = fetch(url2);
    +let c = fetch(url3);
    +
    +Promise.all([a, b, c]).then(values => {
    +  ...
    +});
    + +

    当promise是fullfilled时,传递到履行处理程序的values将包含三个Response对象,每个对象用于已完成的每个fetch()操作。

    + +

    但是,我们不想这样做。我们的代码不关心fetch()操作何时完成。相反,我们想要的是加载的数据。这意味着当我们返回代表图像的可用blob和可用的文本字符串时,我们想要运行Promise.all()块。我们可以编写一个执行此操作的函数;在<script>元素中添加以下内容:

    + +
    function fetchAndDecode(url, type) {
    +  return fetch(url).then(response => {
    +    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: ' + e.message);
    +  });
    +}
    + +

    这看起来有点复杂,所以让我们一步一步地完成它:

    + +
      +
    1. 首先,我们定义函数,向它传递一个URL和字符串,这个字符串表示资源类型。
    2. +
    3. 在函数体内部,我们有一个类似于我们在第一个例子中看到的结构 - 我们调用fetch()函数来获取指定URL处的资源,然后将其链接到另一个 promise ,它解码(或“read”)响应body。这是前一个示例中的blob()方法。
    4. +
    5. 但是,这里有两处不同: +
        +
      • 首先,我们返回的第二个promise会因类型值的不同而不同。在执行函数内部,我们包含一个简单的if ... else if语句,根据我们需要解码的文件类型返回不同的promise(在这种情况下,我们可以选择blobtext,而且很容易扩展这个以处理其他类型)。
      • +
      • 其次,我们在fetch()调用之前添加了return关键字。它的作用是运行整个链,然后运行最终结果(即blob()text()返回的promise作为我们刚刚定义的函数的返回值)。实际上,return语句将结果从链返回到顶部。
      • +
      +
    6. +
    7. +

      在块结束时,我们链接一个.catch()调用,以处理任何可能出现在数组中传递给.all()的任何promise的错误情况。如果任何promise被拒绝,catch块将告诉你哪个promise有问题。 .all()块(见下文)仍然可以实现,但不会显示有问题的资源。如果你想要.all拒绝,你必须将.catch()块链接到那里的末尾。

      +
    8. +
    + +

    函数体内部的代码是async(异步)和基于promise的,因此实际上整个函数就像一个promise ––方便啊!

    +
  6. +
  7. +

    接下来,我们调用我们的函数三次以开始获取和解码图像和文本的过程,并将每个返回的promises存储在变量中。在以前的代码下面添加以下内容:

    + +
    let coffee = fetchAndDecode('coffee.jpg', 'blob');
    +let tea = fetchAndDecode('tea.jpg', 'blob');
    +let description = fetchAndDecode('description.txt', 'text');
    +
  8. +
  9. +

    接下来,我们将定义一个Promise.all()块,仅当上面存储的所有三个promise都已成功完成时才运行一些代码。首先,在.then()调用中添加一个带有空执行程序的块,如下所示:

    + +
    Promise.all([coffee, tea, description]).then(values => {
    +
    +});
    + +

    你可以看到它需要一个包含promises作为参数的数组。执行者只有在所有三个promises的状态成为resolved时才会运行;当发生这种情况时,它将被传入一个数组,其中包含来自各个promise(即解码的响应主体)的结果,类似于 [coffee-results, tea-results, description-results].

    +
  10. +
  11. +

    最后,在执行程序中添加以下内容。这里我们使用一些相当简单的同步代码将结果存储在单独的变量中(从blob创建对象URL),然后在页面上显示图像和文本。

    + +
    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);
    +
  12. +
  13. +

    保存并刷新,你应该看到所有UI组件都已加载,尽管不是特别有吸引力!

    +
  14. +
+ +

我们在这里提供的用于显示项目的代码相当简陋,但现在作为解释器。

+ +
+

注意: 如果你遇到困难,可以将你的代码版本与我们的代码进行比较,看看它的外观 -––see it live,也可以参阅source code

+
+ +
+

注意: 如果你正在改进这段代码,你可能想要遍历一个项目列表来显示,获取和解码每个项目,然后循环遍历Promise.all()内部的结果,运行一个不同的函数来显示每个项目取决于什么代码的类型是。这将使它适用于任何数量的项目,而不仅仅是三个。

+ +

此外,你可以确定要获取的文件类型,而无需显式类型属性。例如,你可以使用response.headers.get("content-type")检查响应的{{HTTPHeader("Content-Type")}} HTTP标头,然后做出相应的反应。

+
+ +

 在promise fullfill/reject后运行一些最终代码

+ +

在promise完成后,你可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,你必须在.then().catch()回调中包含相同的代码,例如:

+ +
myPromise
+.then(response => {
+  doSomething(response);
+  runFinalCode();
+})
+.catch(e => {
+  returnError(e);
+  runFinalCode();
+});
+ +

在现代浏览器中,.finally() 方法可用,它可以链接到常规promise链的末尾,允许你减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:

+ +
myPromise
+.then(response => {
+  doSomething(response);
+})
+.catch(e => {
+  returnError(e);
+})
+.finally(() => {
+  runFinalCode();
+});
+ +

有关一个真实示例,请查看我们的promise-finally.html demo(另请参阅source code)。这与我们在上面部分中看到的Promise.all()演示完全相同,除了在fetchAndDecode()函数中我们将finally()调用链接到链的末尾:

+ +
function fetchAndDecode(url, type) {
+  return fetch(url).then(response => {
+    if(!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    } else {
+      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);
+  })
+  .finally(() => {
+    console.log(`fetch attempt for "${url}" finished.`);
+  });
+}
+ +

这会将一条简单的消息记录到控制台,告诉我们每次获取尝试的时间。

+ +
+

注意:finally()允许你在异步代码中编写异步等价物try/ catch / finally。

+
+ +

 构建自定义promise

+ +

好消息是,在某种程度上,你已经建立了自己的promise。当你使用.then()块链接多个promise时,或者将它们组合起来创建自定义函数时,你已经在创建自己的基于异步声明的自定义函数。例如,从前面的示例中获取我们的fetchAndDecode()函数。

+ +

将不同的基于promise的API组合在一起以创建自定义函数是迄今为止你使用promises进行自定义事务的最常见方式,并展示了基于相同原则的大多数现代API的灵活性和强大功能。然而,还有另一种方式。

+ +

使用Promise()构造函数

+ +

可以使用Promise() 构造函数构建自己的promise。当你需要使用现有的旧项目代码、库或框架以及基于现代promise的代码时,这会派上用场。比如,当你遇到没有使用promise的旧式异步API的代码时,你可以用promise来重构这段异步代码。

+ +

让我们看一个简单的示例来帮助你入门 —— 这里我们用 promise 包装一了个setTimeout()它会在两秒后运行一个函数,该函数将用字符串“Success!”,解析当前promise(调用链接的resolve())。

+ +
let timeoutPromise = new Promise((resolve, reject) => {
+  setTimeout(function(){
+    resolve('Success!');
+  }, 2000);
+});
+ +

resolve()reject()是用来实现拒绝新创建的promise的函数。此处,promise 成功运行通过显示字符串“Success!”。

+ +

因此,当你调用此promise时,可以将.then()块链接到它的末尾,它将传递给.then()块一串“Success!”。在下面的代码中,我们显示出该消息:

+ +
timeoutPromise
+.then((message) => {
+   alert(message);
+})
+ +

更简化的版本:

+ +
timeoutPromise.then(alert);
+
+ +

尝试 running this live 以查看结果 (可参考 source code).

+ +

上面的例子不是很灵活 - promise只能实现一个字符串,并且它没有指定任何类型的reject()条件(诚然,setTimeout()实际上没有失败条件,所以对这个简单的例子并不重要)。

+ +
+

注意: 为什么要resolve(),而不是fullfill()?我们现在给你的答案有些复杂。

+
+ +

拒绝一个自定义promise

+ +

我们可以创建一个reject() 方法拒绝promise  - 就像resolve()一样,这需要一个值,但在这种情况下,它是拒绝的原因,即将传递给.catch()的错误块。

+ +

让我们扩展前面的例子,使其具有一些reject()条件,并允许在成功时传递不同的消息。

+ +

获取previous example副本,并将现有的timeoutPromise()定义替换为:

+ +
function timeoutPromise(message, interval) {
+  return new Promise((resolve, reject) => {
+    if (message === '' || typeof message !== 'string') {
+      reject('Message is empty or not a string');
+    } else if (interval < 0 || typeof interval !== 'number') {
+      reject('Interval is negative or not a number');
+    } else {
+      setTimeout(function(){
+        resolve(message);
+      }, interval);
+    }
+  });
+};
+ +

在这里,我们将两个方法传递给一个自定义函数 - 一个用来做某事的消息,以及在做这件事之前要经过的时间间隔。在函数内部,我们返回一个新的Promise对象 - 调用该函数将返回我们想要使用的promise。

+ +

Promise构造函数中,我们在if ... else结构中进行了一些检查:

+ +
    +
  1. 首先,我们检查消息是否适合被警告。如果它是一个空字符串或根本不是字符串,我们会使用合适的错误消息拒绝该promise。
  2. +
  3. 接下来,我们检查间隔是否是适当的间隔值。如果是负数或不是数字,我们会使用合适的错误消息拒绝promise。
  4. +
  5. 最后,如果参数看起来都正常,我们使用setTimeout()在指定的时间间隔过后,使用指定的消息解析promise。
  6. +
+ +

由于timeoutPromise()函数返回一个Promise,我们可以将.then().catch()等链接到它上面以利用它的功能。现在让我们使用它 - 将以前的timeoutPromise用法替换为以下值:

+ +
timeoutPromise('Hello there!', 1000)
+.then(message => {
+   alert(message);
+})
+.catch(e => {
+  console.log('Error: ' + e);
+});
+ +

当你按原样保存并运行代码时,一秒钟后你将收到消息提醒。现在尝试将消息设置为空字符串或将间隔设置为负数,例如,你将能够通过相应的错误消息查看被拒绝的promise!你还可以尝试使用已解决的消息执行其他操作,而不仅仅是提醒它。

+ +
+

注意: 你可以在GitHub上找到我们的这个示例版本custom-promise2.html(另请参阅source code)。

+
+ +

一个更真实的例子

+ +

上面的例子是故意做得简单,以使概念易于理解,但它并不是实际上完全异步。异步性质基本上是使用setTimeout()伪造的,尽管它仍然表明promises对于创建具有合理的操作流程,良好的错误处理等的自定义函数很有用

+ +

我们想邀请你学习的一个例子是Jake Archibald's idb library,它真正地显示了Promise()构造函数的有用异步应用程序。这采用了 IndexedDB API,它是一种旧式的基于回调的API,用于在客户端存储和检索数据,并允许你将其与promises一起使用。如果你查看main library file,你将看到我们在上面讨论过的相同类型的技术。以下块将许多IndexedDB方法使用的基本请求模型转换为使用promise:

+ +
function promisifyRequest(request) {
+  return new Promise(function(resolve, reject) {
+    request.onsuccess = function() {
+      resolve(request.result);
+    };
+
+    request.onerror = function() {
+      reject(request.error);
+    };
+  });
+}
+ +

这可以通过添加一些事件处理程序来实现,这些事件处理程序在适当的时候实现(fullfill)和拒绝(reject)promise。

+ + + +

总结

+ +

当我们不知道函数的返回值或返回需要多长时间时,Promises是构建异步应用程序的好方法。它们使得在没有深度嵌套回调的情况下更容易表达和推理异步操作序列,并且它们支持类似于同步try ... catch语句的错误处理方式。

+ +

Promise适用于所有现代浏览器的最新版本;promise有兼容问题的唯一情况是Opera Mini和IE11及更早版本。

+ +

本文中,我们没有涉及的所有promise的功能,只是最有趣和最有用的功能。当你开始了解有关promise的更多信息时,你会遇到更多功能和技巧。

+ +

大多数现代Web API都是基于promise的,因此你需要了解promise才能充分利用它们。这些API包括WebRTCWeb Audio APIMedia Capture and Streams等等。随着时间的推移,Promises将变得越来越重要,因此学习使用和理解它们是学习现代JavaScript的重要一步。

+ +

参见

+ + + +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}

+ +

 本章内容

+ + diff --git a/files/zh-cn/learn/javascript/asynchronous/timeouts_and_intervals/index.html b/files/zh-cn/learn/javascript/asynchronous/timeouts_and_intervals/index.html new file mode 100644 index 0000000000..2cf83da82c --- /dev/null +++ b/files/zh-cn/learn/javascript/asynchronous/timeouts_and_intervals/index.html @@ -0,0 +1,617 @@ +--- +title: '合作异步JavaScript: 超时和间隔' +slug: learn/JavaScript/异步/超时和间隔 +translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +--- +
{{LearnSidebar}}
+ +
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}
+ +
+ +

在这里,我们将讨论传统的JavaScript方法,这些方法可以在一段时间或一段规则间隔(例如,每秒固定的次数)之后,以异步方式运行代码,并讨论它们的用处,以及它们的固有问题。

+ + + + + + + + + + + + +
预备条件:基本的计算机知识,对JavaScript基本原理有较好的理解。
目标:了解异步循环和间隔及其用途。
+ +

介绍

+ +

很长一段时间以来,web平台为JavaScript程序员提供了许多函数,这些函数允许您在一段时间间隔过后异步执行代码,或者重复异步执行代码块,直到您告诉它停止为止。这些都是:

+ +
+
setTimeout()
+
在指定的时间后执行一段代码.
+
setInterval()
+
以固定的时间间隔,重复运行一段代码.
+
requestAnimationFrame()
+
setInterval()的现代版本;在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行.
+
+ +

这些函数设置的异步代码实际上在主线程上运行(在其指定的计时器过去之后)。

+ +

在 setTimeout() 调用执行之前或 setInterval() 迭代之间可以(并且经常会)运行其他代码。根据这些操作的处理器密集程度,它们可以进一步延迟异步代码,因为任何异步代码仅在主线程可用后才执行(换句话说,当调用栈为空时)。在阅读本文时,您将学到更多关于此问题的信息。

+ +

无论如何,这些函数用于在web站点或应用程序上运行不间断的动画和其他后台处理。在下面的部分中,我们将向您展示如何使用它们。

+ +

setTimeout()

+ +

正如前述, setTimeout() 在指定的时间后执行一段特定代码. 它需要如下参数:

+ + + +
+

Note: 指定的时间(或延迟)不能保证在指定的确切时间之后执行,而是最短的延迟执行时间。在主线程上的堆栈为空之前,传递给这些函数的回调将无法运行。

+ +

结果,像 setTimeout(fn, 0) 这样的代码将在堆栈为空时立即执行,而不是立即执行。如果执行类似 setTimeout(fn, 0) 之类的代码,之后立即运行从 1 到 100亿 的循环之后,回调将在几秒后执行。 

+
+ +

在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息 (see it running live, and see the source code):

+ +
let myGreeting = setTimeout(function() {
+  alert('Hello, Mr. Universe!');
+}, 2000)
+ +

我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给 setTimeout() 。以下两个版本的代码片段相当于第一个版本:

+ +
// 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);
+ +

例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时。

+ +

setTimeout() 返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 {{anch("Clearing timeouts")}} 。

+ +

传递参数给setTimeout() 

+ +

我们希望传递给setTimeout()中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。

+ +

例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:

+ +
function sayHi(who) {
+  alert('Hello ' + who + '!');
+}
+ +

人名可以通过第三个参数传进 setTimeout()

+ +
let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');
+ +

清除超时

+ +

最后,如果创建了 timeout,您可以通过调用clearTimeout(),将setTimeout()调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:

+ +
clearTimeout(myGreeting);
+ +
+

注意: 请参阅 greeter-app.html 以获得稍微复杂一点的演示,该演示允许您在表单中设置要打招呼的人的姓名,并使用单独的按钮取消问候语(see the source code also)。

+
+ +

setInterval()

+ +

当我们需要在一段时间之后运行一次代码时,setTimeout()可以很好地工作。但是当我们需要反复运行代码时会发生什么,例如在动画的情况下?

+ +

这就是setInterval()的作用所在。这与setTimeout()的工作方式非常相似,只是作为第一个参数传递给它的函数,重复执行的时间不少于第二个参数给出的毫秒数,而不是一次执行。您还可以将正在执行的函数所需的任何参数作为 setInterval() 调用的后续参数传递。

+ +

让我们看一个例子。下面的函数创建一个新的Date()对象,使用toLocaleTimeString()从中提取一个时间字符串,然后在UI中显示它。然后,我们使用setInterval()每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。

+ +

(see this live, and also see the source):

+ +
function displayTime() {
+   let date = new Date();
+   let time = date.toLocaleTimeString();
+   document.getElementById('demo').textContent = time;
+}
+
+const createClock = setInterval(displayTime, 1000);
+ +

setTimeout()一样, setInterval() 返回一个确定的值,稍后你可以用它来取消间隔任务。

+ +

清除intervals

+ +

setInterval()永远保持运行任务,除非我们做点什么——我们可能会想阻止这样的任务,否则当浏览器无法完成任何进一步的任务时我们可能得到错误, 或者动画处理已经完成了。我们可以用与停止超时相同的方法来实现这一点——通过将setInterval()调用返回的标识符传递给clearInterval()函数:

+ +
const myInterval = setInterval(myFunction, 2000);
+
+clearInterval(myInterval);
+ +

主动学习:创建秒表!

+ +

话虽如此,我们还是要给你一个挑战。以我们的setInterval-clock.html为例,修改它以创建您自己的简单秒表。

+ +

你要像前面一样显示时间,但是在这里,你需要:

+ + + +

提示:

+ + + +
+

Note: 如果您在操作过程有困难,请参考 see it runing live (see the source code also).

+
+ +

关于 setTimeout() 和 setInterval() 需要注意的几点

+ +

当使用 setTimeout()setInterval()的时候,有几点需要额外注意。 现在让我们回顾一下:

+ +

递归的timeouts

+ +

还有另一种方法可以使用setTimeout():我们可以递归调用它来重复运行相同的代码,而不是使用setInterval()

+ +

下面的示例使用递归setTimeout()每100毫秒运行传递来的函数:

+ +
let i = 1;
+
+setTimeout(function run() {
+  console.log(i);
+  i++;
+  setTimeout(run, 100);
+}, 100);
+ +

将上面的示例与下面的示例进行比较 ––这使用 setInterval() 来实现相同的效果:

+ +
let i = 1;
+
+setInterval(function run() {
+  console.log(i);
+  i++
+}, 100);
+ +

递归setTimeout()和setInterval()有何不同?

+ +

上述代码的两个版本之间的差异是微妙的。

+ + + +

当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的 setTimeout() - 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。

+ +

立即超时

+ +

使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。

+ +

举个例子,下面的代码(see it live) 输出一个包含警报的"Hello",然后在您点击第一个警报的OK之后立即弹出“world”。

+ +
setTimeout(function() {
+  alert('World');
+}, 0);
+
+alert('Hello');
+ +

如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。

+ +

使用 clearTimeout() or clearInterval()清除

+ +

clearTimeout()clearInterval() 都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除 setTimeout() 和 setInterval()。

+ +

但为了保持一致性,你应该使用 clearTimeout() 来清除 setTimeout() 条目,使用 clearInterval() 来清除 setInterval() 条目。 这样有助于避免混乱。

+ +

requestAnimationFrame()

+ +

requestAnimationFrame() 是一个专门的循环函数,旨在浏览器中高效运行动画。它基本上是现代版本的setInterval() —— 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行,不管其运行的环境如何。

+ +

它是针对setInterval() 遇到的问题创建的,比如 setInterval()并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。

+ +

(在CreativeJS上了解有关此内容的更多信息).

+ +
+

注意: 你可以在课程中其他地方找到requestAnimationFrame() 的使用范例—参见 Drawing graphics, 和 Object building practice

+
+ +

该方法将重新加载页面之前要调用的回调函数作为参数。这是您将看到的常见表达:

+ +
function draw() {
+   // Drawing code goes here
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

这个想法是要定义一个函数,在其中更新动画 (例如,移动精灵,更新乐谱,刷新数据等),然后调用它来开始这个过程。在函数的末尾,以 requestAnimationFrame() 传递的函数作为参数进行调用,这指示浏览器在下一次显示重新绘制时再次调用该函数。然后这个操作连续运行, 因为requestAnimationFrame() 是递归调用的。

+ +
+

注意: 如果要执行某种简单的常规DOM动画, CSS 动画 可能更快,因为它们是由浏览器的内部代码计算而不是JavaScript直接计算的。但是,如果您正在做一些更复杂的事情,并且涉及到在DOM中不能直接访问的对象(such as 2D Canvas API or WebGL objects), requestAnimationFrame() 在大多数情况下是更好的选择。

+
+ +

你的动画跑得有多快?

+ +

动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps)为单位进行测量。这个数字越高,动画看起来就越平滑。

+ +

由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。

+ +

如果您有一个刷新率为60Hz的显示器,并且希望达到60fps,则大约有16.7毫秒(1000/60)来执行动画代码来渲染每个帧。这提醒我们,我们需要注意每次通过动画循环时要运行的代码量。

+ +

requestAnimationFrame() 总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。requestAnimationFrame() 会尽其所能利用现有资源提升帧速率。

+ +

 requestAnimationFrame() 与 setInterval() 和 setTimeout()有什么不同?

+ +

让我们进一步讨论一下 requestAnimationFrame() 方法与前面介绍的其他方法的区别. 下面让我们看一下代码:

+ +
function draw() {
+   // Drawing code goes here
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

现在让我们看看如何使用setInterval():

+ +
function draw() {
+   // Drawing code goes here
+}
+
+setInterval(draw, 17);
+ +

如前所述,我们没有为requestAnimationFrame();指定时间间隔;它只是在当前条件下尽可能快速平稳地运行它。如果动画由于某些原因而处于屏幕外浏览器也不会浪费时间运行它。

+ +

 另一方面setInterval()需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。四舍五入是一个好主意,浏览器可能会尝试运行动画的速度超过60fps,它不会对动画的平滑度有任何影响。如前所述,60Hz是标准刷新率。

+ +

包括时间戳

+ +

传递给 requestAnimationFrame() 函数的实际回调也可以被赋予一个参数(一个时间戳值),表示自 requestAnimationFrame() 开始运行以来的时间。这是很有用的,因为它允许您在特定的时间以恒定的速度运行,而不管您的设备有多快或多慢。您将使用的一般模式如下所示:

+ +
let startTime = null;
+
+function draw(timestamp) {
+    if(!startTime) {
+      startTime = timestamp;
+    }
+
+   currentTime = timestamp - startTime;
+
+   // Do something based on current time
+
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

浏览器支持

+ +

 与setInterval()setTimeout() 相比最近的浏览器支持requestAnimationFrame()

+ +

requestAnimationFrame().在Internet Explorer 10及更高版本中可用。因此,除非您的代码需要支持旧版本的IE,否则没有什么理由不使用requestAnimationFrame() 。

+ +

一个简单的例子

+ +

学习上述理论已经足够了,下面让我们仔细研究并构建自己的requestAnimationFrame() 示例。我们将创建一个简单的“旋转器动画”(spinner animation),即当应用程序忙于连接到服务器时可能会显示的那种动画。

+ +
+

注意: 一般来说,像以下例子中如此简单的动画应用CSS动画来实现,这里使用requestAnimationFrame()只是为了帮助解释其用法。requestAnimationFrame()正常应用于如逐帧更新游戏画面这样的复杂动画。

+
+ +
    +
  1. +

    首先, 下载我们的网页模板

    +
  2. +
  3. +

    放置一个空的 {{htmlelement("div")}} 元素进入 {{htmlelement("body")}}, 然后在其中加入一个 ↻ 字符.这是一个将循环的字符将在我们的例子中作为我们的旋转器(spinner)。

    +
  4. +
  5. +

    用任何你喜欢的方法应用下述的CSS到HTML模板中。这些在页面上设置了一个红色背景,将<body>的高度设置为100%<html>的高度,并将<div>水平和竖直居中。

    + +
    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;
    +}
    +
  6. +
  7. +

    插入一个 {{htmlelement("script")}}元素在 </body> 标签之上。

    +
  8. +
  9. +

    插入下述的JavaScript在你的 <script> 元素中。这里我们存储了一个<div>的引用在一个常量中,设置rotateCount变量为 0, 设置一个未初始化的变量之后将会用作容纳一个requestAnimationFrame() 的调用, 然后设置一个 startTime 变量为 null,它之后将会用作存储 requestAnimationFrame() 的起始时间.。

    + +
    const spinner = document.querySelector('div');
    +let rotateCount = 0;
    +let rAF;
    +let startTime = null;
    +
  10. +
  11. +

    在之前的代码下面, 插入一个 draw() 函数将被用作容纳我们的动画代码,并且包含了 时间戳 参数。

    + +
    function draw(timestamp) {
    +
    +}
    +
  12. +
  13. +

    draw()中, 加入下述的几行。 如果起始时间还没有被赋值的话,将timestamp 传给它(这只将发生在循环中的第一步)。 并赋值给rotateCount ,以旋转 旋转器(spinning)(此处取(当前时间戳 – 起始时间戳) / 3,以免它转得太快):

    + +
      if (!startTime) {
    +   startTime = timestamp;
    +  }
    +
    +  rotateCount = (timestamp - startTime) / 3;
    +
    +
  14. +
  15. +

    draw()内我们刚刚添加的代码之后,添加以下代码 — 此处是在检查rotateCount 的值是否超过了359 (e.g. 360, 一个正圆的度数)。 如果是,与360取模(值除以 360 后剩下的余数),使得圆圈的动画能以合理的低值连续播放。需要注意的是,这样的操作并不是必要的,只是比起类似于“128000度”的值,运行在 0-359 度之间会使你的操作更容易些。

    + +
    if (rotateCount > 359) {
    +  rotateCount %= 360;
    +}
    +
  16. +
  17. +

    接下来,在上一个块下面添加以下行以实际旋转 旋转(spinner)器:

    + +
    spinner.style.transform = `rotate(${rotateCount}deg)`;
    +
  18. +
  19. +

    在函数draw()内的最下方,插入下面这行代码。这是整个操作中最关键的部分 — 我们将前面定义的变量设置为以draw()函数为参数的活动requestAnimation()调用。 这样动画就开始了,以尽可能接近60 FPS的速率不断地运行draw()函数。

    + +
    rAF = requestAnimationFrame(draw);
    +
  20. +
  21. +

    draw() 函数定义下方,添加对 draw() 函数的调用以启动动画。

    +
  22. +
  23. +
    draw();
    +
  24. +
+ +
+

Note: You can find this example live on GitHub (see the source code also).

+
+ +

撤销requestAnimationFrame()

+ +

requestAnimationFrame()可用与之对应的cancelAnimationFrame()方法“撤销”(不同于“set…”类方法的“清除”,此处更接近“撤销”之意)。

+ +

该方法以requestAnimationFrame()的返回值为参数,此处我们将该返回值存在变量 rAF 中:

+ +
cancelAnimationFrame(rAF);
+ +

主动学习: 启动和停止旋转器(spinner)

+ +

在这个练习中,我们希望你能对cancelAnimationFrame()方法做一些测试,在之前的示例中添加新内容。你需要在示例中添加一个事件监听器,用于当鼠标在页面任意位置点击时,启动或停止旋转器。 

+ +

一些提示:

+ + + +
+

Note: 先自己尝试一下,如果你确实遇到困难,可以参看我们的在线示例源代码

+
+ +

限制 (节流) requestAnimationFrame()动画

+ +

requestAnimationFrame() 的限制之一是无法选择帧率。在大多数情况下,这不是问题,因为通常希望动画尽可能流畅地运行。但是,当要创建老式的8位类型的动画时,怎么办?

+ +

例如,在我们的 Drawing Graphics 文章中的猴子岛行走动画中的一个问题:

+ +

{{EmbedGHLiveSample("learning-area/javascript/apis/drawing-graphics/loops_animation/7_canvas_walking_animation.html", '100%', 260)}}

+ +

在此示例中,必须为角色在屏幕上的位置及显示的精灵设置动画。精灵动画中只有6帧。如果通过 requestAnimationFrame() 为屏幕上显示的每个帧显示不同的精灵帧,则 Guybrush 的四肢移动太快,动画看起来很荒谬。因此,此示例使用以下代码限制精灵循环的帧率:

+ +
if (posX % 13 === 0) {
+  if (sprite === 5) {
+    sprite = 0;
+  } else {
+    sprite++;
+  }
+}
+ +

因此,代码每13个动画帧循环一次精灵。

+ +

...实际上,大约是每 6.5 帧,因为我们将每帧更新 posX 2个单位值(角色在屏幕上的位置)

+ +
if(posX > width/2) {
+  newStartPos = -( (width / 2) + 102 );
+  posX = Math.ceil(newStartPos / 13) * 13;
+  console.log(posX);
+} else {
+  posX += 2;
+}
+ +

这是用于计算如何更新每个动画帧中的位置的代码。

+ +

用于限制动画的方法将取决于特定代码。例如,在前面的旋转器实例中,可以通过仅在每帧中增加 rotateCount 一个单位(而不是两个单位)来使其运动速度变慢。

+ +

主动学习:反应游戏

+ +

对于本文的最后部分,将创建一个 2 人反应游戏。游戏将有两个玩家,其中一个使用 A 键控制游戏,另一个使用 L 键控制游戏。

+ +

按下 开始 按钮后,像前面看到的旋转器那样的将显示 5 到 10 秒之间的随机时间。之后将出现一条消息,提示 “PLAYERS GO!!”。一旦这发生,第一个按下其控制按钮的玩家将赢得比赛。

+ +

{{EmbedGHLiveSample("learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html", '100%', 500)}}

+ +

让我们来完成以下工作:

+ +
    +
  1. +

    首先,下载 starter file for the app 。其中包含完成的 HTML 结构和 CSS 样式,为我们提供了一个游戏板,其中显示了两个玩家的信息(如上所示),但 spinner 和 结果段落重叠显示,只需要编写 JavaScript 代码。

    +
  2. +
  3. +

    在页面上空的 {{htmlelement("script")}} 元素里,首先添加以下几行代码,这些代码定义了其余代码中需要的一些常量和变量:

    + +
    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');
    + +

    这些是:

    + +
      +
    1. 对旋转器的引用,因此可以对其进行动画处理。
    2. +
    3. 包含旋转器 {{htmlelement("div")}} 元素的引用。用于显示和隐藏它。
    4. +
    5. 旋转计数。这确定了显示在动画每一帧上的旋转器旋转了多少。
    6. +
    7. 开始时间为null。当旋转器开始旋转时,它将赋值为 开始时间。
    8. +
    9. 一个未初始化的变量,用于之后存储使 旋转器 动画化的  {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}}  调用。
    10. +
    11. 开始按钮的引用。
    12. +
    13. 结果字段的引用。
    14. +
    +
  4. +
  5. +

    接下来,在前几行代码下方,添加以下函数。它只接收两个数字并返回一个在两个数字之间的随机数。稍后将需要它来生成随机超时间隔。

    + +
    function random(min,max) {
    +  var num = Math.floor(Math.random()*(max-min)) + min;
    +  return num;
    +}
    +
  6. +
  7. +

    接下来添加 draw() 函数以使 旋转器 动画化。这与之前的简单旋转器示例中的版本非常相似:

    + +
      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);
    +  }
    +
  8. +
  9. +

    现在是时候在页面首次加载时设置应用程序的初始状态了。添加以下两行,它们使用 display: none; 隐藏结果段落和旋转器容器。

    + +
    result.style.display = 'none';
    +spinnerContainer.style.display = 'none';
    +
  10. +
  11. +

    接下来,定义一个 reset() 函数。该函数在游戏结束后将游戏设置回初始状态以便再次开启游戏。在代码底部添加以下内容:

    + +
    function reset() {
    +  btn.style.display = 'block';
    +  result.textContent = '';
    +  result.style.display = 'none';
    +}
    +
  12. +
  13. 好的,准备充分!现在该使游戏变得可玩了!将以下代码块添加到代码中。 start() 函数调用 draw() 以启动 旋转器,并在UI中显示它,隐藏“开始”按钮,这样您就无法通过同时启动多次来弄乱游戏,并运行一个经过5到10秒的随机间隔后,会运行 setEndgame() 函数的 setTimeout() 。下面的代码块还将一个事件侦听器添加到按钮上,以在单击它时运行 start() 函数。 +
    btn.addEventListener('click', start);
    +
    +function start() {
    +  draw();
    +  spinnerContainer.style.display = 'block';
    +  btn.style.display = 'none';
    +  setTimeout(setEndgame, random(5000,10000));
    +}
    +
  14. +
  15. 添加以下方法到代码:
  16. +
+ +
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);
+  };
+}
+ +

逐步执行以下操作:

+ +
    +
  1. 首先通过 {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}}  取消 旋转器 动画(清理不必要的流程总是一件好事),隐藏 旋转器 容器。
  2. +
  3. 接下来,显示结果段落并将其文本内容设置为“ PLAYERS GO !!”。向玩家发出信号,表示他们现在可以按下按钮来取胜。
  4. +
  5. keydown 事件侦听器附加到 document。当按下任何按钮时,keyHandler() 函数将运行。
  6. +
  7. 在 keyHandler() 里,在 keyHandler() 内部,代码包括作为参数的事件对象作为参数(用 e 表示)- 其 {{domxref("KeyboardEvent.key", "key")}} 属性包含刚刚按下的键,可以通过这个对象来对特定的操作和特定的按键做出响应。
  8. +
  9. 将变量 isOver 设置为 false ,这样我们就可以跟踪是否按下了正确的按键以使玩家1或2获胜。我们不希望游戏在按下错误的键后结束。
  10. +
  11. e.key 输出到控制台,这是找出所按的不同键的键值的有用方法。
  12. +
  13. e.key 为“ a”时,显示一条消息说玩家1获胜;当 e.key 为“ l”时,显示消息说玩家2获胜。 (注意:这仅适用于小写的a和l - 如果提交了大写的 A 或 L(键加上 Shift),则将其视为另一个键!)如果按下了其中一个键,请将 isOver 设置为 true
  14. +
  15. 仅当 isOvertrue 时,才使用 {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} 删除 keydown 事件侦听器,以便一旦产生获胜的按键,就不再有键盘输入可以弄乱最终游戏结果。您还可以使用 setTimeout() 在5秒钟后调用 reset()-如前所述,此函数将游戏重置为原始状态,以便可以开始新游戏。
  16. +
+ +

就这样-一切都完成了!

+ +
+

Note: 如果卡住了, check out our version of the reaction game (see the source code also).

+
+ +

结论

+ +

就是这样-异步循环和间隔的所有要点在一篇文章中介绍了。您会发现这些方法在许多情况下都很有用,但请注意不要过度使用它们!因为它们仍然在主线程上运行,所以繁重的回调(尤其是那些操纵DOM的回调)会在不注意的情况下降低页面的速度。

+ +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}

+ +

In this module

+ + -- cgit v1.2.3-54-g00ecf