From 03e2f89170237b85d52ebc867dd3b812a3661d6b Mon Sep 17 00:00:00 2001 From: ian7525 Date: Sat, 1 May 2021 10:31:33 +0800 Subject: Add /learn/javascript/asynchronous/ folder, zh-TW --- .../javascript/asynchronous/async_await/index.html | 435 ++++++++++++++ .../choosing_the_right_approach/index.html | 536 +++++++++++++++++ .../javascript/asynchronous/concepts/beachball.jpg | Bin 0 -> 7495 bytes .../javascript/asynchronous/concepts/index.html | 163 ++++++ .../zh-tw/learn/javascript/asynchronous/index.html | 66 +++ .../javascript/asynchronous/introducing/index.html | 287 +++++++++ .../javascript/asynchronous/promises/index.html | 600 +++++++++++++++++++ .../asynchronous/timeouts_and_intervals/index.html | 646 +++++++++++++++++++++ 8 files changed, 2733 insertions(+) create mode 100644 files/zh-tw/learn/javascript/asynchronous/async_await/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/choosing_the_right_approach/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/concepts/beachball.jpg create mode 100644 files/zh-tw/learn/javascript/asynchronous/concepts/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/introducing/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/promises/index.html create mode 100644 files/zh-tw/learn/javascript/asynchronous/timeouts_and_intervals/index.html (limited to 'files/zh-tw') diff --git a/files/zh-tw/learn/javascript/asynchronous/async_await/index.html b/files/zh-tw/learn/javascript/asynchronous/async_await/index.html new file mode 100644 index 0000000000..3c594daf2a --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/async_await/index.html @@ -0,0 +1,435 @@ +--- +title: 利用 async 及 await 讓非同步程式設計變得更容易 +slug: Learn/JavaScript/Asynchronous/Async_await +tags: + - Beginner + - CodingScripting + - Guide + - JavaScript + - Learn + - Promises + - async + - asynchronous + - await +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}
+ +

More recent additions to the JavaScript language are async functions and the await keyword, part of the so-called ECMAScript 2017 JavaScript edition (see ECMAScript Next support in Mozilla). These features basically act as syntactic sugar on top of promises, making asynchronous code easier to write and to read afterwards. They make async code look more like old-school synchronous code, so they're well worth learning. This article gives you what you need to know.

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals, an understanding of async code in general and promises.
Objective:To understand promises and how to use them.
+ +

The basics of async/await

+ +

There are two parts to using async/await in your code.

+ +

The async keyword

+ +

First of all we have the async keyword, which you put in front of a function declaration to turn it into an async function. An async function is a function that knows how to expect the possibility of the await keyword being used to invoke asynchronous code.

+ +

Try typing the following lines into your browser's JS console:

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

The function returns "Hello" — nothing special, right?

+ +

But what if we turn this into an async function? Try the following:

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

Ah. Invoking the function now returns a promise. This is one of the traits of async functions — their return values are guaranteed to be converted to promises.

+ +

You can also create an async function expression, like so:

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

And you can use arrow functions:

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

These all do basically the same thing.

+ +

To actually consume the value returned when the promise fulfills, since it is returning a promise, we could use a .then() block:

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

or even just shorthand such as

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

Like we saw in the last article.

+ +

So the async keyword is added to functions to tell them to return a promise rather than directly returning the value.

+ +

The await keyword

+ +

The advantage of an async function only becomes apparent when you combine it with the await keyword. await only works inside async functions within regular JavaScript code, however it can be used on its own with JavaScript modules.

+ +

await can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.

+ +

You can use await when calling any function that returns a Promise, including web API functions.

+ +

Here is a trivial example:

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

Of course, the above example is not very useful, although it does serve to illustrate the syntax. Let's move on and look at a real example.

+ +

Rewriting promise code with async/await

+ +

Let's look back at a simple fetch example that we saw in the previous article:

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

By now, you should have a reasonable understanding of promises and how they work, but let's convert this to use async/await to see how much simpler it makes things:

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

It makes code much simpler and easier to understand — no more .then() blocks everywhere!

+ +

Since an async keyword turns a function into a promise, you could refactor your code to use a hybrid approach of promises and await, bringing the second half of the function out into a new block to make it more flexible:

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

You can try typing in the example yourself, or running our live example (see also the source code).

+ +

But how does it work?

+ +

You'll note that we've wrapped the code inside a function, and we've included the async keyword before the function keyword. This is necessary — you have to create an async function to define a block of code in which you'll run your async code; as we said earlier, await only works inside of async functions.

+ +

Inside the myFetch() function definition you can see that the code closely resembles the previous promise version, but there are some differences. Instead of needing to chain a .then() block on to the end of each promise-based method, you just need to add an await keyword before the method call, and then assign the result to a variable. The await keyword causes the JavaScript runtime to pause your code on this line, not allowing further code to execute in the meantime until the async function call has returned its result — very useful if subsequent code relies on that result!

+ +

Once that's complete, your code continues to execute starting on the next line. For example:

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

The response returned by the fulfilled fetch() promise is assigned to the response variable when that response becomes available, and the parser pauses on this line until that occurs. Once the response is available, the parser moves to the next line, which creates a Blob out of it. This line also invokes an async promise-based method, so we use await here as well. When the result of operation returns, we return it out of the myFetch() function.

+ +

This means that when we call the myFetch() function, it returns a promise, so we can chain a .then() onto the end of it inside which we handle displaying the blob onscreen.

+ +

You are probably already thinking "this is really cool!", and you are right — fewer .then() blocks to wrap around code, and it mostly just looks like synchronous code, so it is really intuitive.

+ +

Adding error handling

+ +

And if you want to add error handling, you've got a couple of options.

+ +

You can use a synchronous try...catch structure with async/await. This example expands on the first version of the code we showed above:

+ +
async function myFetch() {
+  try {
+    let response = await fetch('coffee.jpg');
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+    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();
+ +

The catch() {} block is passed an error object, which we've called e; we can now log that to the console, and it will give us a detailed error message showing where in the code the error was thrown.

+ +

If you wanted to use the second (refactored) version of the code that we showed above, you would be better off just continuing the hybrid approach and chaining a .catch() block onto the end of the .then() call, like this:

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

This is because the .catch() block will catch errors occurring in both the async function call and the promise chain. If you used the try/catch block here, you might still get unhandled errors in the myFetch() function when it's called.

+ +

You can find both of these examples on GitHub:

+ + + +

Awaiting a Promise.all()

+ +

async/await is built on top of promises, so it's compatible with all the features offered by promises. This includes Promise.all() — you can quite happily await a Promise.all() call to get all the results returned into a variable in a way that looks like simple synchronous code. Again, let's return to an example we saw in our previous article. Keep it open in a separate tab so you can compare and contrast with the new version shown below.

+ +

Converting this to async/await (see live demo and source code), this now looks like so:

+ +
async function fetchAndDecode(url, type) {
+  let response = await fetch(url);
+
+  let content;
+
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
+  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)
+);
+ +

You'll see that the fetchAndDecode() function has been converted easily into an async function with just a few changes. See the Promise.all() line:

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

By using await here we are able to get all the results of the three promises returned into the values array, when they are all available, in a way that looks very much like sync code. We've had to wrap all the code in a new async function, displayContent(), and we've not reduced the code by a lot of lines, but being able to move the bulk of the code out of the .then() block provides a nice, useful simplification, leaving us with a much more readable program.

+ +

For error handling, we've included a .catch() block on our displayContent() call; this will handle errors occurring in both functions.

+ +
+

Note: It is also possible to use a sync finally block within an async function, in place of a .finally() async block, to show a final report on how the operation went — you can see this in action in our live example (see also the source code).

+
+ +

The downsides of async/await

+ +

Async/await is really useful to know about, but there are a couple of downsides to consider.

+ +

Async/await makes your code look synchronous, and in a way it makes it behave more synchronously. The await keyword blocks execution of all the code that follows it until the promise fulfills, exactly as it would with a synchronous operation. It does allow other tasks to continue to run in the meantime, but the awaited code is blocked.

+ +
async function makeResult(items) {
+ let newArr = [];
+ for(let i=0; i < items.length; i++) {
+  newArr.push('word_'+i);
+ }
+ return newArr;
+}
+
+async function getResult() {
+ let result = await makeResult(items); // Blocked on this line
+ useThatResult(result); // Will not be executed before makeResult() is done
+}
+
+
+ +

This means that your code could be slowed down by a significant number of awaited promises happening straight after one another. Each await will wait for the previous one to finish, whereas actually what you want is for the promises to begin processing simultaneously, like they would do if we weren't using async/await.

+ +

There is a pattern that can mitigate this problem — setting off all the promise processes by storing the Promise objects in variables, and then awaiting them all afterwards. Let's have a look at some examples that prove the concept.

+ +

We've got two examples available — slow-async-await.html (see source code) and fast-async-await.html (see source code). Both of them start off with a custom promise function that fakes an async process with a setTimeout() call:

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

Then each one includes a timeTest() async function that awaits three timeoutPromise() calls:

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

Each one ends by recording a start time, seeing how long the timeTest() promise takes to fulfill, then recording an end time and reporting how long the operation took in total:

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

It is the timeTest() function that differs in each case.

+ +

In the slow-async-await.html example, timeTest() looks like this:

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

Here we await all three timeoutPromise() calls directly, making each one alert for 3 seconds. Each subsequent one is forced to wait until the last one finished — if you run the first example, you'll see the alert box reporting a total run time of around 9 seconds.

+ +

In the fast-async-await.html example, timeTest() looks like this:

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

Here we store the three Promise objects in variables, which has the effect of setting off their associated processes all running simultaneously.

+ +

Next, we await their results — because the promises all started processing at essentially the same time, the promises will all fulfill at the same time; when you run the second example, you'll see the alert box reporting a total run time of just over 3 seconds!

+ +

You'll have to test your code carefully, and bear this in mind if performance starts to suffer.

+ +

Another minor inconvenience is that you have to wrap your awaited promises inside an async function.

+ +

Async/await class methods

+ +

As a final note before we move on, you can even add async in front of class/object methods to make them return promises, and await promises inside them. Take a look at the ES class code we saw in our object-oriented JavaScript article, and then look at our modified version with an async method:

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

The first class method could now be used something like this:

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

Browser support

+ +

One consideration when deciding whether to use async/await is support for older browsers. They are available in modern versions of most browsers, the same as promises; the main support problems come with Internet Explorer and Opera Mini.

+ +

If you want to use async/await but are concerned about older browser support, you could consider using the BabelJS library — this allows you to write your applications using the latest JavaScript and let Babel figure out what changes if any are needed for your user’s browsers. On encountering a browser that does not support async/await, Babel's polyfill can automatically provide fallbacks that work in older browsers.

+ +

Conclusion

+ +

And there you have it — async/await provide a nice, simplified way to write async code that is simpler to read and maintain. Even with browser support being more limited than other async code mechanisms at the time of writing, it is well worth learning and considering for use, both for now and in the future.

+ +

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

+ +

In this module

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/choosing_the_right_approach/index.html b/files/zh-tw/learn/javascript/asynchronous/choosing_the_right_approach/index.html new file mode 100644 index 0000000000..6099d470fa --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/choosing_the_right_approach/index.html @@ -0,0 +1,536 @@ +--- +title: 選擇正確的方法 +slug: Learn/JavaScript/Asynchronous/Choosing_the_right_approach +tags: + - Beginner + - Intervals + - JavaScript + - Learn + - Optimize + - Promises + - async + - asynchronous + - await + - requestAnimationFrame + - setInterval + - setTimeout + - timeouts +--- +
{{LearnSidebar}}
+ +
{{PreviousMenu("Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}
+ +

在結束本單元前,我們將會從先前所討論過的不同程式技巧和功能,幫助您考量在甚麼情況下應該使用哪一個,並適當的提供一些有關常見的陷阱的建議及提醒。

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective:To be able to make a sound choice of when to use different asynchronous programming techniques.
+ +

Asynchronous callbacks

+ +

Generally found in old-style APIs, involves a function being passed into another function as a parameter, which is then invoked when an asynchronous operation has been completed, so that the callback can in turn do something with the result. This is the precursor to promises; it's not as efficient or flexible. Use only when necessary.

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

Code example

+ +

An example that loads a resource via the XMLHttpRequest API (run it live, and 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);
+ +

Pitfalls

+ + + +

Browser compatibility

+ +

Really good general support, although the exact support for callbacks in APIs depends on the particular API. Refer to the reference documentation for the API you're using for more specific support info.

+ +

Further information

+ + + +

setTimeout()

+ +

setTimeout() is a method that allows you to run a function after an arbitrary amount of time has passed.

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

Code example

+ +

Here the browser will wait two seconds before executing the anonymous function, then will display the alert message (see it running live, and see the source code):

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

Pitfalls

+ +

You can use recursive setTimeout() calls to run a function repeatedly in a similar fashion to setInterval(), using code like this:

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

There is a difference between recursive setTimeout() and setInterval():

+ + + +

When your code has the potential to take longer to run than the time interval you’ve assigned, it’s better to use recursive setTimeout() — this will keep the time interval constant between executions regardless of how long the code takes to execute, and you won't get errors.

+ +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

setInterval()

+ +

setInterval() is a method that allows you to run a function repeatedly with a set interval of time between each execution. Not as efficient as requestAnimationFrame(), but allows you to choose a running rate/frame rate.

+ + + + + + + + + + + + + + + + + +
Useful for...
Single delayed operationRepeating operationMultiple sequential operationsMultiple simultaneous operations
NoYesNo (unless they are the same)No
+ +

Code example

+ +

The following function creates a new Date() object, extracts a time string out of it using toLocaleTimeString(), and then displays it in the UI. We then run it once per second using setInterval(), creating the effect of a digital clock that updates once per second (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);
+ +

Pitfalls

+ + + +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

requestAnimationFrame()

+ +

requestAnimationFrame() is a method that allows you to run a function repeatedly, and efficiently, at the best framerate available given the current browser/system. You should, if at all possible, use this instead of setInterval()/recursive setTimeout(), unless you need a specific framerate.

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

Code example

+ +

A simple animated spinner; you can find this example live on GitHub (see the source code also):

+ +
const spinner = document.querySelector('div');
+let rotateCount = 0;
+let startTime = null;
+let rAF;
+
+function draw(timestamp) {
+    if(!startTime) {
+        startTime = timestamp;
+    }
+
+    rotateCount = (timestamp - startTime) / 3;
+
+    if(rotateCount > 359) {
+        rotateCount %= 360;
+    }
+
+    spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
+
+    rAF = requestAnimationFrame(draw);
+}
+
+draw();
+ +

Pitfalls

+ + + +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

Promises

+ +

Promises are a JavaScript feature that allows you to run asynchronous operations and wait until it is definitely complete before running another operation based on its result. Promises are the backbone of modern asynchronous JavaScript.

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

Code example

+ +

The following code fetches an image from the server and displays it inside an {{htmlelement("img")}} element; see it live also, and see also the 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);
+});
+ +

Pitfalls

+ +

Promise chains can be complex and hard to parse. If you nest a number of promises, you can end up with similar troubles to callback hell. For example:

+ +
remotedb.allDocs({
+  include_docs: true,
+  attachments: true
+}).then(function (result) {
+  let 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...
+ +

It is better to use the chaining power of promises to go with a flatter, easier to parse structure:

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

or even:

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

That covers a lot of the basics. For a much more complete treatment, see the excellent We have a problem with promises, by Nolan Lawson.

+ +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

Promise.all()

+ +

A JavaScript feature that allows you to wait for multiple promises to complete before then running a further operation based on the results of all the other promises.

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

Code example

+ +

The following example fetches several resources from the server, and uses Promise.all() to wait for all of them to be available before then displaying all of them — see it live, and see the 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);
+});
+ +

Pitfalls

+ + + +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

Async/await

+ +

Syntactic sugar built on top of promises that allows you to run asynchronous operations using syntax that's more like writing synchronous callback code.

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

Code example

+ +

The following example is a refactor of the simple promise example we saw earlier that fetches and displays an image, written using async/await (see it live, and see the 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();
+ +

Pitfalls

+ + + +

Browser compatibility

+ +

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

+ +

Further information

+ + + +

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

+ +

In this module

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/concepts/beachball.jpg b/files/zh-tw/learn/javascript/asynchronous/concepts/beachball.jpg new file mode 100644 index 0000000000..e116b4e8ea Binary files /dev/null and b/files/zh-tw/learn/javascript/asynchronous/concepts/beachball.jpg differ diff --git a/files/zh-tw/learn/javascript/asynchronous/concepts/index.html b/files/zh-tw/learn/javascript/asynchronous/concepts/index.html new file mode 100644 index 0000000000..067ebfdf54 --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/concepts/index.html @@ -0,0 +1,163 @@ +--- +title: 非同步程式設計通用概念 +slug: Learn/JavaScript/Asynchronous/Concepts +tags: + - JavaScript + - Learn + - Promises + - Threads + - asynchronous + - blocking +--- +
{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}
+ +

在本篇文章我們會介紹一些關於非同步程式設計的重要觀念,以及在網頁瀏覽器和 JavaScript 中的行為。在閱讀其他文章之前您應該先具備這些觀念。

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective:To understand the basic concepts behind asynchronous programming, and how they manifest in web browsers and JavaScript.
+ +

Asynchronous?

+ +

Normally, a given program's code runs straight along, with only one thing happening at once. If a function relies on the result of another function, it has to wait for the other function to finish and return, and until that happens, the entire program is essentially stopped from the perspective of the user.

+ +

Mac users, for example, sometimes experience this as the spinning rainbow-colored cursor (or "beachball" as it is often called). This cursor is how the operating system says "the current program you're using has had to stop and wait for something to finish up, and it's taking so long that I was worried you'd wonder what was going on."

+ +

Multi-colored macOS beachball busy spinner

+ +

This is a frustrating experience and isn't a good use of computer processing power — especially in an era in which computers have multiple processor cores available. There's no sense sitting there waiting for something when you could let the other task chug along on another processor core and let you know when it's done. This lets you get other work done in the meantime, which is the basis of asynchronous programming. It is up to the programming environment you are using (web browsers, in the case of web development) to provide you with APIs that allow you to run such tasks asynchronously.

+ +

Blocking code

+ +

Asynchronous techniques are very useful, particularly in web programming. When a web app runs in a browser and it executes an intensive chunk of code without returning control to the browser, the browser can appear to be frozen. This is called blocking; the browser is blocked from continuing to handle user input and perform other tasks until the web app returns control of the processor.

+ +

Let's look at a couple of examples that show what we mean by blocking.

+ +

In our simple-sync.html example (see it running live), we add a click event listener to a button so that when clicked, it runs a time-consuming operation (calculates 10 million dates then logs the final one to the console) and then adds a paragraph to the 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);
+});
+ +

When running the example, open your JavaScript console then click the button — you'll notice that the paragraph does not appear until after the dates have finished being calculated and the console message has been logged. The code runs in the order it appears in the source, and the later operation doesn't run till the earlier operation has finished running.

+ +
+

Note: The previous example is very unrealistic. You would never calculate 10 million dates on a real web app! It does, however, serve to give you the basic idea.

+
+ +

In our second example, simple-sync-ui-blocking.html (see it live), we simulate something slightly more realistic that you might come across on a real page. We block user interactivity with the rendering of the UI. In this example, we have two buttons:

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

If you click the first button and then quickly click the second one, you'll see that the alert does not appear until the circles have finished being rendered. The first operation blocks the second one until it has finished running.

+ +
+

Note: OK, in our case, it is ugly and we are faking the blocking effect, but this is a common problem that developers of real apps fight to mitigate all the time.

+
+ +

Why is this? The answer is because JavaScript, generally speaking, is single-threaded. At this point, we need to introduce the concept of threads.

+ +

Threads

+ +

A thread is basically a single process that a program can use to complete tasks. Each thread can only do a single task at once:

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

Each task will be run sequentially; a task has to complete before the next one can be started.

+ +

As we said earlier, many computers now have multiple cores, so can do multiple things at once. Programming languages that can support multiple threads can use multiple cores to complete multiple tasks simultaneously:

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

JavaScript is single-threaded

+ +

JavaScript is traditionally single-threaded. Even with multiple cores, you could only get it to run tasks on a single thread, called the main thread. Our example from above is run like this:

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

After some time, JavaScript gained some tools to help with such problems. Web workers allow you to send some of the JavaScript processing off to a separate thread, called a worker so that you can run multiple JavaScript chunks simultaneously. You'd generally use a worker to run expensive processes off the main thread so that user interaction is not blocked.

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

With this in mind, have a look at simple-sync-worker.html (see it running live), again with your browser's JavaScript console open. This is a rewrite of our previous example that calculates the 10 million dates in a separate worker thread. Now when you click the button, the browser is able to display the paragraph before the dates have finished calculating. The first operation no longer blocks the second.

+ +

Asynchronous code

+ +

Web workers are pretty useful, but they do have their limitations. A major one is they are not able to access the {{Glossary("DOM")}} — you can't get a worker to directly do anything to update the UI. We couldn't render our 1 million blue circles inside our worker; it can basically just do the number crunching.

+ +

The second problem is that although code run in a worker is not blocking, it is still basically synchronous. This becomes a problem when a function relies on the results of multiple previous processes to function. Consider the following thread diagrams:

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

In this case, let's say Task A is doing something like fetching an image from the server and Task B then does something to the image like applying a filter to it. If you start Task A running and then immediately try to run Task B, you'll get an error, because the image won't be available yet.

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

In this case, let's say Task D makes use of the results of both Task B and Task C. If we can guarantee that these results will both be available at the same time, then we might be OK, but this is unlikely. If Task D tries to run when one of its inputs is not yet available, it will throw an error.

+ +

To fix such problems, browsers allow us to run certain operations asynchronously. Features like Promises allow you to set an operation running (e.g. the fetching of an image from the server), and then wait until the result has returned before running another operation:

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

Since the operation is happening somewhere else, the main thread is not blocked while the async operation is being processed.

+ +

We'll start to look at how we can write asynchronous code in the next article. Exciting stuff, huh? Keep reading!

+ +

Conclusion

+ +

Modern software design increasingly revolves around using asynchronous programming, to allow programs to do more than one thing at a time. As you use newer and more powerful APIs, you'll find more cases where the only way to do things is asynchronously. It used to be hard to write asynchronous code. It still takes getting used to, but it's gotten a lot easier. In the rest of this module, we'll explore further why asynchronous code matters and how to design code that avoids some of the problems described above.

+ +
{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}
+ +

In this module

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/index.html b/files/zh-tw/learn/javascript/asynchronous/index.html new file mode 100644 index 0000000000..f1279adeee --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/index.html @@ -0,0 +1,66 @@ +--- +title: 非同步的 JavaScript +slug: Learn/JavaScript/Asynchronous +tags: + - Beginner + - CodingScripting + - Guide + - JavaScript + - Landing + - Promises + - async + - asynchronous + - await + - callbacks + - requestAnimationFrame + - setInterval + - setTimeout + - 非同步的 +--- +
{{LearnSidebar}}
+ +

在本單元我們來討論非同步的 ({{Glossary("asynchronous")}}) {{Glossary("JavaScript")}} ,為何其如此重要,並了解它如何有效率的處理像是從伺服器獲取資源的這類潛在性阻塞 (blocking) 操作

+ +
+

想要成為前端開發人員?

+ +

我們已為您準備一門實現您目標所需要具備的所有基礎知識課程

+ +

立即開始

+ +
+ +

事前準備

+ +

非同步的 JavaScript 是一個相當進階的主題,因此建議您在嘗試本單元前能先通過 JavaScript 初探 以及 JavaScript 構成元素 單元。

+ +

如果您對非同步程式設計的概念還不太熟悉,強烈建議您應該先從 非同步程式設計通用概念 的文章開始學習。如果您已經具備其概念,那麼您或許可以跳至 非同步的 JavaScript 介紹 單元開始。

+ +
+

Note: 如果您正在使用電腦/平板/任何其它無法讓您建立檔案的裝置上,您可以嘗試在 JSBin 或是 Glitch 上測試本單元的範例程式碼。

+
+ +

Guides

+ +
+
非同步程式設計通用概念
+
+

在本篇文章我們會介紹一些關於非同步程式設計的重要觀念,以及在網頁瀏覽器和 JavaScript 中的行為。在閱讀其他文章之前您應該先具備這些觀念。

+
+
非同步的 JavaScript 介紹
+
在本篇文章中我們會先簡短的回顧我們在同步的 JavaScript 中所遭遇到的問題,並預先看看稍後將會使用哪些非同步的 JavaScript 技巧來解決此問題。
+
協同的非同步 JavaScript: Timeouts 和 intervals
+
在這裡看看我們在傳統上是如何透過設定的時間到期或是透過定時的方式 (如每秒發生的次數) 讓 Javascript 能夠以非同步的方式執行程式碼,談談這些方法有哪些用處以及存在哪些既有的問題。
+
優雅的使用 Promises 來處理非同步操作
+
Promises 是在 Javascript 語言中相對較新的功能,它能夠讓你延遲活動直到先前的活動回報完成或失敗。這方法對設置一連串的操作並讓其正確的循序執行相當有用。本篇文章向您展示 promises 是如何運作,您將會看到如何被使用在 WebAPIs,以及如何寫出屬於自己的 promises。
+
利用 async 及 await 讓非同步程式設計變得更容易
+
Promises 在設置上可能會有些複雜並難以理解,因此現代瀏覽器已經實作出 async 函式以及 await 運算子。前者能夠讓標準的函式隱含的使用 promises 方式來實現非同步行為,然而後者可以被用在 async 函式內部,讓程式碼繼續執行之前去等待一個 promises 完成。這能讓我們在鏈結一連串的 promises 的情況之下更加簡潔易懂。
+
選擇正確的方法
+
在結束本單元前,我們將會從先前所討論過的不同程式技巧和功能,幫助您考量在甚麼情況下應該使用哪一個,並適當的提供一些有關常見的陷阱的建議及提醒。
+
+ +

參閱

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/introducing/index.html b/files/zh-tw/learn/javascript/asynchronous/introducing/index.html new file mode 100644 index 0000000000..adaf89c906 --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/introducing/index.html @@ -0,0 +1,287 @@ +--- +title: 非同步的 JavaScript 介紹 +slug: Learn/JavaScript/Asynchronous/Introducing +tags: + - Beginner + - CodingScripting + - Guide + - Introducing + - JavaScript + - Learn + - Promises + - async + - asynchronous + - await + - callbacks +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Concepts", "Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous")}}
+ +

在本篇文章中我們會先簡短的回顧我們在同步的 JavaScript 中所遭遇到的問題,並預先看看稍後將會使用哪些非同步的 JavaScript 技巧來解決此問題。

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective:To gain familiarity with what asynchronous JavaScript is, how it differs from synchronous JavaScript, and what use cases it has.
+ +

Synchronous JavaScript

+ +

To allow us to understand what {{Glossary("asynchronous")}} JavaScript is, we ought to start off by making sure we understand what {{Glossary("synchronous")}} JavaScript is. This section recaps some of the information we saw in the previous article.

+ +

A lot of the functionality we have looked at in previous learning area modules is synchronous — you run some code, and the result is returned as soon as the browser can do so. Let's look at a simple example (see it live here, and see the source):

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

In this block, the lines are executed one after the other:

+ +
    +
  1. We grab a reference to a {{htmlelement("button")}} element that is already available in the DOM.
  2. +
  3. We add a click event listener to it so that when the button is clicked: +
      +
    1. An alert() message appears.
    2. +
    3. Once the alert is dismissed, we create a {{htmlelement("p")}} element.
    4. +
    5. We then give it some text content.
    6. +
    7. Finally, we append the paragraph to the document body.
    8. +
    +
  4. +
+ +

While each operation is being processed, nothing else can happen — rendering is paused. This is because as we said in the previous article, JavaScript is single threaded. Only one thing can happen at a time, on a single main thread, and everything else is blocked until an operation completes.

+ +

So in the example above, after you've clicked the button the paragraph won't appear until after the OK button is pressed in the alert box. You can try it for yourself:

+ + + +

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

+ +
+

Note: It is important to remember that alert(), while being very useful for demonstrating a synchronous blocking operation, is terrible for use in real world applications.

+
+ +

Asynchronous JavaScript

+ +

For reasons illustrated earlier (e.g. related to blocking), many Web API features now use asynchronous code to run, especially those that access or fetch some kind of resource from an external device, such as fetching a file from the network, accessing a database and returning data from it, accessing a video stream from a web cam, or broadcasting the display to a VR headset.

+ +

Why is this difficult to get to work using synchronous code? Let's look at a quick example. When you fetch an image from a server, you can't return the result immediately. That means that the following (pseudocode) wouldn't work:

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

That's because you don't know how long the image will take to download, so when you come to run the second line it will throw an error (possibly intermittently, possibly every time) because the response is not yet available. Instead, you need your code to wait until the response is returned before it tries to do anything else to it.

+ +

There are two main types of asynchronous code style you'll come across in JavaScript code, old-style callbacks and newer promise-style code. In the below sections we'll review each of these in turn.

+ +

Async callbacks

+ +

Async callbacks are functions that are specified as arguments when calling a function which will start executing code in the background. When the background code finishes running, it calls the callback function to let you know the work is done, or to let you know that something of interest has happened. Using callbacks is slightly old-fashioned now, but you'll still see them in use in a number of older-but-still-commonly-used APIs.

+ +

An example of an async callback is the second parameter of the {{domxref("EventTarget.addEventListener", "addEventListener()")}} method (as we saw in action above):

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

The first parameter is the type of event to be listened for, and the second parameter is a callback function that is invoked when the event is fired.

+ +

When we pass a callback function as an argument to another function, we are only passing the function's reference as an argument, i.e, the callback function is not executed immediately. It is “called back” (hence the name) asynchronously somewhere inside the containing function’s body. The containing function is responsible for executing the callback function when the time comes.

+ +

You can write your own function containing a callback easily enough. Let's look at another example that loads a resource via the XMLHttpRequest API (run it live, and 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);
+ +

Here we create a displayImage() function that represents a blob passed to it as an object URL, then creates an image to display the URL in, appending it to the document's <body>. However, we then create a loadAsset() function that takes a callback as a parameter, along with a URL to fetch and a content type. It uses XMLHttpRequest (often abbreviated to "XHR") to fetch the resource at the given URL, then pass the response to the callback to do something with. In this case the callback is waiting on the XHR call to finish downloading the resource (using the onload event handler) before it passes it to the callback.

+ +

Callbacks are versatile — not only do they allow you to control the order in which functions are run and what data is passed between them, they also allow you to pass data to different functions depending on circumstance. So you could have different actions to run on the response downloaded, such as processJSON(), displayText(), etc.

+ +

Note that not all callbacks are async — some run synchronously. An example is when we use {{jsxref("Array.prototype.forEach()")}} to loop through the items in an array (see it live, and the source):

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

In this example we loop through an array of Greek gods and print the index numbers and values to the console. The expected parameter of forEach() is a callback function, which itself takes two parameters, a reference to the array name and index values. However, it doesn't wait for anything — it runs immediately.

+ +

Promises

+ +

Promises are the new style of async code that you'll see used in modern Web APIs. A good example is the fetch() API, which is basically like a modern, more efficient version of {{domxref("XMLHttpRequest")}}. Let's look at a quick example, from our Fetching data from the server article:

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

Note: You can find the finished version on GitHub (see the source here, and also see it running live).

+
+ +

Here we see fetch() taking a single parameter — the URL of a resource you want to fetch from the network — and returning a promise. The promise is an object representing the completion or failure of the async operation. It represents an intermediate state, as it were. In essence, it's the browser's way of saying "I promise to get back to you with the answer as soon as I can," hence the name "promise."

+ +

This concept can take practice to get used to; it feels a little like {{interwiki("wikipedia", "Schrödinger's cat")}} in action. Neither of the possible outcomes have happened yet, so the fetch operation is currently waiting on the result of the browser trying to complete the operation at some point in the future. We've then got three further code blocks chained onto the end of the fetch():

+ + + +
+

Note: You'll learn a lot more about promises later on in the module, so don't worry if you don't understand them fully yet.

+
+ +

The event queue

+ +

Async operations like promises are put into an event queue, which runs after the main thread has finished processing so that they do not block subsequent JavaScript code from running. The queued operations will complete as soon as possible then return their results to the JavaScript environment.

+ +

Promises versus callbacks

+ +

Promises have some similarities to old-style callbacks. They are essentially a returned object to which you attach callback functions, rather than having to pass callbacks into a function.

+ +

However, promises are specifically made for handling async operations, and have many advantages over old-style callbacks:

+ + + +

The nature of asynchronous code

+ +

Let's explore an example that further illustrates the nature of async code, showing what can happen when we are not fully aware of code execution order and the problems of trying to treat asynchronous code like synchronous code. The following example is fairly similar to what we've seen before (see it live, and the source). One difference is that we've included a number of {{domxref("console.log()")}} statements to illustrate an order that you might think the code would execute in.

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

The browser will begin executing the code, see the first console.log() statement (Starting) and execute it, and then create the image variable.

+ +

It will then move to the next line and begin executing the fetch() block but, because fetch() executes asynchronously without blocking, code execution continues after the promise-related code, thereby reaching the final console.log() statement (All done!) and outputting it to the console.

+ +

Only once the fetch() block has completely finished running and delivering its result through the .then() blocks will we finally see the second console.log() message (It worked :)) appear. So the messages have appeared in a different order to what you might expect:

+ + + +

If this confuses you, then consider the following smaller example:

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

This is very similar in behavior — the first and third console.log() messages will be shown immediately, but the second one is blocked from running until someone clicks the mouse button. The previous example works in the same way, except that in that case the second message is blocked on the promise chain fetching a resource then displaying it on screen, rather than a click.

+ +

In a less trivial code example, this kind of setup could cause a problem — you can't include an async code block that returns a result, which you then rely on later in a sync code block. You just can't guarantee that the async function will return before the browser has processed the sync block.

+ +

To see this in action, try taking a local copy of our example, and changing the fourth console.log() call to the following:

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

You should now get an error in your console instead of the third message:

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

This is because at the time the browser tries to run the third console.log() statement, the fetch() block has not finished running so the image variable has not been given a value.

+ +
+

Note: For security reasons, you can't fetch() files from your local filesystem (or run other such operations locally); to run the above example locally you'll have to run the example through a local webserver.

+
+ +

Active learning: make it all async!

+ +

To fix the problematic fetch() example and make the three console.log() statements appear in the desired order, you could make the third console.log() statement run async as well. This can be done by moving it inside another .then() block chained onto the end of the second one, or by moving it inside the second then() block. Try fixing this now.

+ +
+

Note: If you get stuck, you can find an answer here (see it running live also). You can also find a lot more information on promises in our Graceful asynchronous programming with Promises guide, later on in the module.

+
+ +

Conclusion

+ +

In its most basic form, JavaScript is a synchronous, blocking, single-threaded language, in which only one operation can be in progress at a time. But web browsers define functions and APIs that allow us to register functions that should not be executed synchronously, and should instead be invoked asynchronously when some kind of event occurs (the passage of time, the user's interaction with the mouse, or the arrival of data over the network, for example). This means that you can let your code do several things at the same time without stopping or blocking your main thread.

+ +

Whether we want to run code synchronously or asynchronously will depend on what we're trying to do.

+ +

There are times when we want things to load and happen right away. For example when applying some user-defined styles to a webpage you'll want the styles to be applied as soon as possible.

+ +

If we're running an operation that takes time however, like querying a database and using the results to populate templates, it is better to push this off the main thread and complete the task asynchronously. Over time, you'll learn when it makes more sense to choose an asynchronous technique over a synchronous one.

+ +

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

+ +

In this module

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/promises/index.html b/files/zh-tw/learn/javascript/asynchronous/promises/index.html new file mode 100644 index 0000000000..cbac933963 --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/promises/index.html @@ -0,0 +1,600 @@ +--- +title: 優雅的使用 Promises 來處理非同步操作 +slug: Learn/JavaScript/Asynchronous/Promises +tags: + - Beginner + - CodingScripting + - Guide + - JavaScript + - Learn + - Promises + - async + - asynchronous + - catch + - finally + - then +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}
+ +

Promises 是在 Javascript 語言中相對較新的功能,它能夠讓你延遲活動直到先前的活動回報完成或失敗。這方法對設置一連串的操作並讓其正確的循序執行相當有用。本篇文章向您展示 promises 是如何運作,您將會看到如何被使用在 WebAPIs,以及如何寫出屬於自己的 promises。

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective:To understand promises and how to use them.
+ +

What are promises?

+ +

We looked at Promises briefly in the first article of the course, but here we'll look at them in a lot more depth.

+ +

Essentially, a Promise is an object that represents an intermediate state of an operation — in effect, a promise that a result of some kind will be returned at some point in the future. There is no guarantee of exactly when the operation will complete and the result will be returned, but there is a guarantee that when the result is available, or the promise fails, the code you provide will be executed in order to do something else with a successful result, or to gracefully handle a failure case.

+ +

Generally, you are less interested in the amount of time an async operation will take to return its result (unless of course, it takes far too long!), and more interested in being able to respond to it being returned, whenever that is. And of course, it's nice that it doesn't block the rest of the code execution.

+ +

One of the most common engagements you'll have with promises is with web APIs that return a promise. Let's consider a hypothetical video chat application. The application has a window with a list of the user's friends, and clicking on a button next to a user starts a video call to that user.

+ +

That button's handler calls {{domxref("MediaDevices.getUserMedia", "getUserMedia()")}} in order to get access to the user's camera and microphone. Since getUserMedia() has to ensure that the user has permission to use those devices and ask the user which microphone to use and which camera to use (or whether to be a voice-only call, among other possible options), it can block until not only all of those decisions are made, but also the camera and microphone have been engaged. Also, the user may not respond immediately to these permission requests. This can potentially take a long time.

+ +

Since the call to getUserMedia() is made from the browser's main thread, the entire browser is blocked until getUserMedia() returns! Obviously, that's not an acceptable option; without promises, everything in the browser becomes unusable until the user decides what to do about the camera and microphone. So instead of waiting for the user, getting the chosen devices enabled, and directly returning the {{domxref("MediaStream")}} for the stream created from the selected sources, getUserMedia() returns a {{jsxref("promise")}} which is resolved with the {{domxref("MediaStream")}} once it's available.

+ +

The code that the video chat application would use might look something like this:

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

This function starts by using a function called setStatusMessage() to update a status display with the message "Calling...", indicating that a call is being attempted. It then calls getUserMedia(), asking for a stream that has both video and audio tracks, then once that's been obtained, sets up a video element to show the stream coming from the camera as a "self view," then takes each of the stream's tracks and adds them to the WebRTC {{domxref("RTCPeerConnection")}} representing a connection to another user. After that, the status display is updated to say "Connected".

+ +

If getUserMedia() fails, the catch block runs. This uses setStatusMessage() to update the status box to indicate that an error occurred.

+ +

The important thing here is that the getUserMedia() call returns almost immediately, even if the camera stream hasn't been obtained yet. Even if the handleCallButton() function has already returned to the code that called it, when getUserMedia() has finished working, it calls the handler you provide. As long as the app doesn't assume that streaming has begun, it can just keep on running.

+ +
+

Note: You can learn more about this somewhat advanced topic, if you're interested, in the article Signaling and video calling. Code similar to this, but much more complete, is used in that example.

+
+ +

The trouble with callbacks

+ +

To fully understand why promises are a good thing, it helps to think back to old-style callbacks and to appreciate why they are problematic.

+ +

Let's talk about ordering pizza as an analogy. There are certain steps that you have to take for your order to be successful, which doesn't really make sense to try to execute out of order, or in order but before each previous step has quite finished:

+ +
    +
  1. You choose what toppings you want. This can take a while if you are indecisive, and may fail if you just can't make up your mind, or decide to get a curry instead.
  2. +
  3. You then place your order. This can take a while to return a pizza and may fail if the restaurant does not have the required ingredients to cook it.
  4. +
  5. You then collect your pizza and eat. This might fail if, say, you forgot your wallet so can't pay for the pizza!
  6. +
+ +

With old-style callbacks, a pseudo-code representation of the above functionality might look something like this:

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

This is messy and hard to read (often referred to as "callback hell"), requires the failureCallback() to be called multiple times (once for each nested function), with other issues besides.

+ +

Improvements with promises

+ +

Promises make situations like the above much easier to write, parse, and run. If we represented the above pseudo-code using asynchronous promises instead, we'd end up with something like this:

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

This is much better — it is easier to see what is going on, we only need a single .catch() block to handle all the errors, it doesn't block the main thread (so we can keep playing video games while we wait for the pizza to be ready to collect), and each operation is guaranteed to wait for previous operations to complete before running. We're able to chain multiple asynchronous actions to occur one after another this way because each .then() block returns a new promise that resolves when the .then() block is done running. Clever, right?

+ +

Using arrow functions, you can simplify the code even further:

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

Or even this:

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

This works because with arrow functions () => x is valid shorthand for () => { return x; }.

+ +

You could even do this, since the functions just pass their arguments directly, so there isn't any need for that extra layer of functions:

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

This is not quite as easy to read, however, and this syntax might not be usable if your blocks are more complex than what we've shown here.

+ +
+

Note: You can make further improvements with async/await syntax, which we'll dig into in the next article.

+
+ +

At their most basic, promises are similar to event listeners, but with a few differences:

+ + + +

Explaining basic promise syntax: A real example

+ +

Promises are important to understand because most modern Web APIs use them for functions that perform potentially lengthy tasks. To use modern web technologies you'll need to use promises. Later on in the chapter, we'll look at how to write your own promise, but for now, we'll look at some simple examples that you'll encounter in Web APIs.

+ +

In the first example, we'll use the fetch() method to fetch an image from the web, the {{domxref("Body.blob", "blob()")}} method to transform the fetch response's raw body contents into a {{domxref("Blob")}} object, and then display that blob inside an {{htmlelement("img")}} element. This is very similar to the example we looked at in the first article of the series, but we'll do it a bit differently as we get you building your own promise-based code.

+ +
+

Note: The following example will not work if you just run it directly from the file (i.e. via a file:// URL). You need to run it through a local testing server, or use an online solution such as Glitch or GitHub pages.

+
+ +
    +
  1. +

    First of all, download our simple HTML template and the sample image file that we'll fetch.

    +
  2. +
  3. +

    Add a {{htmlelement("script")}} element at the bottom of the HTML {{htmlelement("body")}}.

    +
  4. +
  5. +

    Inside your {{HTMLElement("script")}} element, add the following line:

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

    This calls the fetch() method, passing it the URL of the image to fetch from the network as a parameter. This can also take an options object as an optional second parameter, but we are just using the simplest version for now. We are storing the promise object returned by fetch() inside a variable called promise. As we said before, this object represents an intermediate state that is initially neither success nor failure — the official term for a promise in this state is pending.

    +
  6. +
  7. +

    To respond to the successful completion of the operation whenever that occurs (in this case, when a {{domxref("Response")}} is returned), we invoke the .then() method of the promise object. The callback inside the .then() block runs only when the promise call completes successfully and returns the {{domxref("Response")}} object — in promise-speak, when it has been fulfilled. It is passed the returned {{domxref("Response")}} object as a parameter.

    + +
    +

    Note: The way that a .then() block works is similar to when you add an event listener to an object using AddEventListener(). It doesn't run until an event occurs (when the promise fulfills). The most notable difference is that a .then() will only run once for each time it is used, whereas an event listener could be invoked multiple times.

    +
    + +

    We immediately run the blob() method on this response to ensure that the response body is fully downloaded, and when it is available transform it into a Blob object that we can do something with. The result of this is returned like so:

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

    which is shorthand for

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

    Unfortunately, we need to do slightly more than this. Fetch promises do not fail on 404 or 500 errors — only on something catastrophic like a network failure. Instead, they succeed, but with the response.ok property set to false. To produce an error on a 404, for example, we need to check the value of response.ok, and if false, throw an error, only returning the blob if it is true. This can be done like so — add the following lines below your first line of JavaScript.

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

    Each call to .then() creates a new promise. This is very useful; because the blob() method also returns a promise, we can handle the Blob object it returns on fulfillment by invoking the .then() method of the second promise. Because we want to do something a bit more complex to the blob than just run a single method on it and return the result, we'll need to wrap the function body in curly braces this time (otherwise it'll throw an error).

    + +

    Add the following to the end of your code:

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

    Now let's fill in the body of the .then() callback. Add the following lines inside the curly braces:

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

    Here we are running the {{domxref("URL.createObjectURL()")}} method, passing it as a parameter the Blob returned when the second promise fulfills. This will return a URL pointing to the object. Then we create an {{htmlelement("img")}} element, set its src attribute to equal the object URL and append it to the DOM, so the image will display on the page!

    +
  12. +
+ +

If you save the HTML file you've just created and load it in your browser, you'll see that the image is displayed in the page as expected. Good work!

+ +
+

Note: You will probably notice that these examples are somewhat contrived. You could just do away with the whole fetch() and blob() chain, and just create an <img> element and set its src attribute value to the URL of the image file, coffee.jpg. We did, however, pick this example because it demonstrates promises in a nice simple fashion, rather than for its real-world appropriateness.

+
+ +

Responding to failure

+ +

Something is missing — currently, there is nothing to explicitly handle errors if one of the promises fails (rejects, in promise-speak). We can add error handling by running the .catch() method off the previous promise. Add this now:

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

To see this in action, try misspelling the URL to the image and reloading the page. The error will be reported in the console of your browser's developer tools.

+ +

This doesn't do much more than it would if you just didn't bother including the .catch() block at all, but think about it — this allows us to control error handling exactly how we want. In a real app, your .catch() block could retry fetching the image, or show a default image, or prompt the user to provide a different image URL, or whatever.

+ +
+

Note: You can see our version of the example live (see the source code also).

+
+ +

Chaining the blocks together

+ +

This is a very longhand way of writing this out; we've deliberately done this to help you understand what is going on clearly. As shown earlier on in the article, you can chain together .then() blocks (and also .catch() blocks). The above code could also be written like this (see also simple-fetch-chained.html on GitHub):

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

Bear in mind that the value returned by a fulfilled promise becomes the parameter passed to the next .then() block's callback function.

+ +
+

Note: .then()/.catch() blocks in promises are basically the async equivalent of a try...catch block in sync code. Bear in mind that synchronous try...catch won't work in async code.

+
+ +

Promise terminology recap

+ +

There was a lot to cover in the above section, so let's go back over it quickly to give you a short guide that you can bookmark and use to refresh your memory in the future. You should also go over the above section again a few more time to make sure these concepts stick.

+ +
    +
  1. When a promise is created, it is neither in a success or failure state. It is said to be pending.
  2. +
  3. When a promise returns, it is said to be resolved. +
      +
    1. A successfully resolved promise is said to be fulfilled. It returns a value, which can be accessed by chaining a .then() block onto the end of the promise chain. The callback function inside the .then() block will contain the promise's return value.
    2. +
    3. An unsuccessful resolved promise is said to be rejected. It returns a reason, an error message stating why the promise was rejected. This reason can be accessed by chaining a .catch() block onto the end of the promise chain.
    4. +
    +
  4. +
+ +

Running code in response to multiple promises fulfilling

+ +

The above example showed us some of the real basics of using promises. Now let's look at some more advanced features. For a start, chaining processes to occur one after the other is all fine, but what if you want to run some code only after a whole bunch of promises have all fulfilled?

+ +

You can do this with the ingeniously named Promise.all() static method. This takes an array of promises as an input parameter and returns a new Promise object that will fulfil only if and when all promises in the array fulfil. It looks something like this:

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

If they all fulfil, the chained .then() block's callback function will be passed an array containing all those results as a parameter. If any of the promises passed to Promise.all() reject, the whole block will reject.

+ +

This can be very useful. Imagine that we’re fetching information to dynamically populate a UI feature on our page with content. In many cases, it makes sense to receive all the data and only then show the complete content, rather than displaying partial information.

+ +

Let's build another example to show this in action.

+ +
    +
  1. +

    Download a fresh copy of our page template, and again put a <script> element just before the closing </body> tag.

    +
  2. +
  3. +

    Download our source files (coffee.jpg, tea.jpg, and description.txt), or feel free to substitute your own.

    +
  4. +
  5. +

    In our script, we'll first define a function that returns the promises we want to send to Promise.all(). This would be easy if we just wanted to run the Promise.all() block in response to three fetch() operations completing. We could just do something like:

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

    When the promise is fulfilled, the values passed into the fulfillment handler would contain three Response objects, one for each of the fetch() operations that have completed.

    + +

    However, we don't want to do this. Our code doesn't care when the fetch() operations are done. Instead, what we want is the loaded data. That means we want to run the Promise.all() block when we get back usable blobs representing the images, and a usable text string. We can write a function that does this; add the following inside your <script> element:

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

    This looks a bit complex, so let's run through it step by step:

    + +
      +
    1. First of all, we define the function, passing it a URL and a string representing the type of resource it is fetching.
    2. +
    3. Inside the function body, we have a similar structure to what we saw in the first example — we call the fetch() function to fetch the resource at the specified URL, then chain it onto another promise that returns the decoded (or "read") response body. This was always the blob() method in the previous example.
    4. +
    5. However, two things are different here: +
        +
      • First of all, the second promise we return is different depending on what the type value is. Inside the .then() callback function, we include a simple if ... else if statement to return a different promise depending on what type of file we need to decode (in this case we've got a choice of blob or text, but it would be easy to extend this to deal with other types as well).
      • +
      • Second, we have added the return keyword before the fetch() call. The effect this has is to run the entire chain and then run the final result (i.e. the promise returned by blob() or text()) as the return value of the function we've just defined. In effect, the return statements pass the results back up the chain to the top.
      • +
      +
    6. +
    7. +

      At the end of the block, we chain on a .catch() call, to handle any error cases that may occur with any of the promises passed in the array to .all(). If any of the promises reject, the .catch() block will let you know which one had a problem. The .all() block (see below) will still fulfill, but it won't display the resources that had problems. Remember that, once you handle the promise with a  .catch() block, the resulting promise is considered resolved but with a value of undefined; that's why in this case the .all() block will always get fulfilled. If you wanted the .all() to reject, you'd have to chain the .catch() block on to the end of the .all() instead. 

      +
    8. +
    + +

    The code inside the function body is async and promise-based, therefore in effect, the entire function acts like a promise — convenient.

    +
  6. +
  7. +

    Next, we call our function three times to begin the process of fetching and decoding the images and text and store each of the returned promises in a variable. Add the following below your previous code:

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

    Next, we will define a Promise.all() block to run some code only when all three of the promises stored above have successfully fulfilled. To begin with, add a block with an empty callback function inside the .then() call, like so:

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

    You can see that it takes an array containing the promises as a parameter. The .then() callback function will only run when all three promises resolve; when that happens, it will be passed an array containing the results from the individual promises (i.e. the decoded response bodies), kind of like [coffee-results, tea-results, description-results].

    +
  10. +
  11. +

    Finally, add the following inside the callback. Here we use some fairly simple sync code to store the results in separate variables (creating object URLs from the blobs), then display the images and text on the page.

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

    Save and refresh and you should see your UI components all loaded, albeit in a not particularly attractive way!

    +
  14. +
+ +

The code we provided here for displaying the items is fairly rudimentary but works as an explainer for now.

+ +
+

Note: If you get stuck, you can compare your version of the code to ours, to see what it is meant to look like — see it live, and see the source code.

+
+ +
+

Note: If you were improving this code, you might want to loop through a list of items to display, fetching and decoding each one, and then loop through the results inside Promise.all(), running a different function to display each one depending on what the type of code was. This would make it work for any number of items, not just three.

+ +

Also, you could determine what the type of file is being fetched without needing an explicit type property. You could, for example, check the {{HTTPHeader("Content-Type")}} HTTP header of the response in each case using response.headers.get("content-type"), and then react accordingly.

+
+ +

Running some final code after a promise fulfills/rejects

+ +

There will be cases where you want to run a final block of code after a promise completes, regardless of whether it fulfilled or rejected. Previously you'd have to include the same code in both the .then() and .catch() callbacks, for example:

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

In more recent modern browsers, the .finally() method is available, which can be chained onto the end of your regular promise chain allowing you to cut down on code repetition and do things more elegantly. The above code can now be written as follows:

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

For a real example, take a look at our promise-finally.html demo (see the source code also). This works the same as the Promise.all() demo we looked at in the above section, except that in the fetchAndDecode() function we chain a finally() call on to the end of the chain:

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

This logs a simple message to the console to tell us when each fetch attempt has finished.

+ +
+

Note: then()/catch()/finally() is the async equivalent to try/catch/finally in sync code.

+
+ +

Building your own custom promises

+ +

The good news is that, in a way, you've already built your own promises. When you've chained multiple promises together with .then() blocks, or otherwise combined them to create custom functionality, you are already making your own custom async promise-based functions. Take our fetchAndDecode() function from the previous examples, for example.

+ +

Combining different promise-based APIs together to create custom functionality is by far the most common way you'll do custom things with promises, and shows the flexibility and power of basing most modern APIs around the same principle. There is another way, however.

+ +

Using the Promise() constructor

+ +

It is possible to build your own promises using the Promise() constructor. The main situation in which you'll want to do this is when you've got code based on an old-school asynchronous API that is not promise-based, which you want to promisify. This comes in handy when you need to use existing, older project code, libraries, or frameworks along with modern promise-based code.

+ +

Let's have a look at a simple example to get you started — here we wrap a setTimeout() call with a promise — this runs a function after two seconds that resolves the promise (using the passed resolve() call) with a string of "Success!".

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

resolve() and reject() are functions that you call to fulfil or reject the newly-created promise. In this case, the promise fulfills with a string of "Success!".

+ +

So when you call this promise, you can chain a .then() block onto the end of it and it will be passed a string of "Success!". In the below code we alert that message:

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

or even just

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

Try running this live to see the result (also see the source code).

+ +

The above example is not very flexible — the promise can only ever fulfil with a single string, and it doesn't have any kind of reject() condition specified (admittedly, setTimeout() doesn't really have a fail condition, so it doesn't matter for this simple example).

+ +
+

Note: Why resolve(), and not fulfill()? The answer we'll give you, for now, is it's complicated.

+
+ +

Rejecting a custom promise

+ +

We can create a promise that rejects using the reject() method — just like resolve(), this takes a single value, but in this case, it is the reason to reject with, i.e., the error that will be passed into the .catch() block.

+ +

Let's extend the previous example to have some reject() conditions as well as allowing different messages to be passed upon success.

+ +

Take a copy of the previous example, and replace the existing timeoutPromise() definition with this:

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

Here we are passing two arguments into a custom function — a message to do something with, and the time interval to pass before doing the thing. Inside the function we then return a new Promise object — invoking the function will return the promise we want to use.

+ +

Inside the Promise constructor, we do several checks inside if ... else structures:

+ +
    +
  1. First of all, we check to see if the message is appropriate for being alerted. If it is an empty string or not a string at all, we reject the promise with a suitable error message.
  2. +
  3. Next, we check to see if the interval is an appropriate interval value. If it is negative or not a number, we reject the promise with a suitable error message.
  4. +
  5. Finally, if the parameters both look OK, we resolve the promise with the specified message after the specified interval has passed using setTimeout().
  6. +
+ +

Since the timeoutPromise() function returns a Promise, we can chain .then(), .catch(), etc. onto it to make use of its functionality. Let's use it now — replace the previous timeoutPromise usage with this one:

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

When you save and run the code as is, after one second you'll get the message alerted. Now try setting the message to an empty string or the interval to a negative number, for example, and you'll be able to see the promise reject with the appropriate error messages! You could also try doing something else with the resolved message rather than just alerting it.

+ +
+

Note: You can find our version of this example on GitHub as custom-promise2.html (see also the source code).

+
+ +

A more real-world example

+ +

The above example was kept deliberately simple to make the concepts easy to understand, but it is not really very async. The asynchronous nature is basically faked using setTimeout(), although it does still show that promises are useful for creating a custom function with a sensible flow of operations, good error handling, etc.

+ +

One example we'd like to invite you to study, which does show a useful async application of the Promise() constructor, is Jake Archibald's idb library. This takes the IndexedDB API, which is an old-style callback-based API for storing and retrieving data on the client-side, and allows you to use it with promises. In the code you'll see the same kind of techniques we discussed above being used there. The following block converts the basic request model used by many IndexedDB methods to use promises (see this code, for example).

+ +

Conclusion

+ +

Promises are a good way to build asynchronous applications when we don’t know the return value of a function or how long it will take to return. They make it easier to express and reason about sequences of asynchronous operations without deeply nested callbacks, and they support a style of error handling that is similar to the synchronous try...catch statement.

+ +

Promises work in the latest versions of all modern browsers; the only place where promise support will be a problem is in Opera Mini and IE11 and earlier versions.

+ +

We didn't touch on all promise features in this article, just the most interesting and useful ones. As you start to learn more about promises, you'll come across further features and techniques.

+ +

Most modern Web APIs are promise-based, so you'll need to understand promises to get the most out of them. Among those APIs are WebRTC, Web Audio API, Media Capture and Streams, and many more. Promises will be more and more important as time goes on, so learning to use and understand them is an important step in learning modern JavaScript.

+ +

See also

+ + + +

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

+ +

In this module

+ + diff --git a/files/zh-tw/learn/javascript/asynchronous/timeouts_and_intervals/index.html b/files/zh-tw/learn/javascript/asynchronous/timeouts_and_intervals/index.html new file mode 100644 index 0000000000..9e076bcad4 --- /dev/null +++ b/files/zh-tw/learn/javascript/asynchronous/timeouts_and_intervals/index.html @@ -0,0 +1,646 @@ +--- +title: '協同的非同步 JavaScript: Timeouts 和 intervals' +slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +tags: + - Animation + - Beginner + - CodingScripting + - Guide + - Intervals + - JavaScript + - Loops + - asynchronous + - requestAnimationFrame + - setInterval + - setTimeout + - timeouts +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}
+ +

在這裡看看我們在傳統上是如何透過設定的時間到期或是透過定時的方式 (如每秒發生的次數) 讓 Javascript 能夠以非同步的方式執行程式碼,談談這些方法有哪些用處以及存在哪些既有的問題。

+ + + + + + + + + + + + +
Prerequisites:Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective:To understand asynchronous loops and intervals and what they are useful for.
+ +

Introduction

+ +

For a long time, the web platform has offered JavaScript programmers a number of functions that allow them to asynchronously execute code after a certain time interval has elapsed, and to repeatedly execute a block of code asynchronously until you tell it to stop.

+ +

These functions are:

+ +
+
setTimeout()
+
Execute a specified block of code once after a specified time has elapsed.
+
setInterval()
+
Execute a specified block of code repeatedly with a fixed time delay between each call.
+
requestAnimationFrame()
+
The modern version of setInterval(). Executes a specified block of code before the browser next repaints the display, allowing an animation to be run at a suitable framerate regardless of the environment it is being run in.
+
+ +

The asynchronous code set up by these functions runs on the main thread (after their specified timer has elapsed).

+ +

It's important to know that you can (and often will) run other code before a setTimeout() call executes, or between iterations of setInterval(). Depending on how processor-intensive these operations are, they can delay your async code even further, as any async code will execute only after the main thread is available. (In other words, when the stack is empty.)  You will learn more on this matter as you progress through this article.

+ +

In any case, these functions are used for running constant animations and other background processing on a web site or application. In the following sections we will show you how they can be used.

+ +

setTimeout()

+ +

As we said before, setTimeout() executes a particular block of code once after a specified time has elapsed. It takes the following parameters:

+ + + +
+

NOTE: The specified amount of time (or the delay) is not the guaranteed time to execution, but rather the minimum time to execution. The callbacks you pass to these functions cannot run until the stack on the main thread is empty.

+ +

As a consequence, code like setTimeout(fn, 0) will execute as soon as the stack is empty, not immediately. If you execute code like setTimeout(fn, 0) but then immediately after run a loop that counts from 1 to 10 billion, your callback will be executed after a few seconds.

+
+ +

In the following example, the browser will wait two seconds before executing the anonymous function, then will display the alert message (see it running live, and see the source code):

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

The functions you specify don't have to be anonymous. You can give your function a name, and even define it somewhere else and pass a function reference to the setTimeout(). The following two versions of the code snippet are equivalent to the first one:

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

That can be useful if you have a function that needs to be called both from a timeout and in response to an event, for example. But it can also just help keep your code tidy, especially if the timeout callback is more than a few lines of code.

+ +

setTimeout() returns an identifier value that can be used to refer to the timeout later, such as when you want to stop it. See {{anch("Clearing timeouts")}} (below) to learn how to do that.

+ +

Passing parameters to a setTimeout() function

+ +

Any parameters that you want to pass to the function being run inside the setTimeout() must be passed to it as additional parameters at the end of the list.

+ +

For example, you could refactor the previous function so that it will say hi to whatever person's name is passed to it:

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

Now, you can pass the name of the person into the setTimeout() call as a third parameter:

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

Clearing timeouts

+ +

Finally, if a timeout has been created, you can cancel it before the specified time has elapsed by calling clearTimeout(), passing it the identifier of the setTimeout() call as a parameter. So to cancel our above timeout, you'd do this:

+ +
clearTimeout(myGreeting);
+ +
+

Note: See greeter-app.html for a slightly more involved demo that allows you to set the name of the person to say hello to in a form, and cancel the greeting using a separate button (see the source code also).

+
+ +

setInterval()

+ +

setTimeout() works perfectly when you need to run code once after a set period of time. But what happens when you need to run the code over and over again—for example, in the case of an animation?

+ +

This is where setInterval() comes in. This works in a very similar way to setTimeout(), except that the function you pass as the first parameter is executed repeatedly at no less than the number of milliseconds given by the second parameter apart, rather than once. You can also pass any parameters required by the function being executed as subsequent parameters of the setInterval() call.

+ +

Let's look at an example. The following function creates a new Date() object, extracts a time string out of it using toLocaleTimeString(), and then displays it in the UI. It then runs the function once per second using setInterval(), creating the effect of a digital clock that updates once per second (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);
+ +

Just like setTimeout(), setInterval() returns an identifying value you can use later when you need to clear the interval.

+ +

Clearing intervals

+ +

setInterval() keeps running a task forever, unless you do something about it. You'll probably want a way to stop such tasks, otherwise you may end up getting errors when the browser can't complete any further versions of the task, or if the animation being handled by the task has finished. You can do this the same way you stop timeouts — by passing the identifier returned by the setInterval() call to the clearInterval() function:

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

Active learning: Creating your own stopwatch!

+ +

With this all said, we've got a challenge for you. Take a copy of our setInterval-clock.html example, and modify it to create your own simple stopwatch.

+ +

You need to display a time as before, but in this example, you need:

+ + + +

Here's a few hints for you:

+ + + +
+

Note: If you get stuck, you can find our version here (see the source code also).

+
+ +

Things to keep in mind about setTimeout() and setInterval()

+ +

There are a few things to keep in mind when working with setTimeout() and setInterval(). Let's review these now.

+ +

Recursive timeouts

+ +

There is another way to use setTimeout(): you can call it recursively to run the same code repeatedly, instead of using setInterval().

+ +

The below example uses a recursive setTimeout() to run the passed function every 100 milliseconds:

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

Compare the above example to the following one — this uses setInterval() to accomplish the same effect:

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

How do recursive setTimeout() and setInterval() differ?

+ +

The difference between the two versions of the above code is a subtle one.

+ + + +

When your code has the potential to take longer to run than the time interval you’ve assigned, it’s better to use recursive setTimeout() — this will keep the time interval constant between executions regardless of how long the code takes to execute, and you won't get errors.

+ +

Immediate timeouts

+ +

Using 0 as the value for setTimeout() schedules the execution of the specified callback function as soon as possible but only after the main code thread has been run.

+ +

For instance, the code below (see it live) outputs an alert containing "Hello", then an alert containing "World" as soon as you click OK on the first alert.

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

This can be useful in cases where you want to set a block of code to run as soon as all of the main thread has finished running — put it on the async event loop, so it will run straight afterwards.

+ +

Clearing with clearTimeout() or clearInterval()

+ +

clearTimeout() and clearInterval() both use the same list of entries to clear from. Interestingly enough, this means that you can use either method to clear a setTimeout() or setInterval().

+ +

For consistency, you should use clearTimeout() to clear setTimeout() entries and clearInterval() to clear setInterval() entries. This will help to avoid confusion.

+ +

requestAnimationFrame()

+ +

requestAnimationFrame() is a specialized looping function created for running animations efficiently in the browser. It is basically the modern version of setInterval() — it executes a specified block of code before the browser next repaints the display, allowing an animation to be run at a suitable frame rate regardless of the environment it is being run in.

+ +

It was created in response to perceived problems with setInterval(), which for example doesn't run at a frame rate optimized for the device, sometimes drops frames, continues to run even if the tab is not the active tab or the animation is scrolled off the page, etc.

+ +

(Read more about this on CreativeJS.)

+ +
+

Note: You can find examples of using requestAnimationFrame() elsewhere in the course — see for example Drawing graphics, and Object building practice.

+
+ +

The method takes as an argument a callback to be invoked before the repaint. This is the general pattern you'll see it used in:

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

The idea is to define a function in which your animation is updated (e.g. your sprites are moved, score is updated, data is refreshed, or whatever). Then, you call it to start the process off. At the end of the function block you call requestAnimationFrame() with the function reference passed as the parameter, and this instructs the browser to call the function again on the next display repaint. This is then run continuously, as the code is calling requestAnimationFrame() recursively.

+ +
+

Note: If you want to perform some kind of simple constant DOM animation, CSS Animations are probably faster. They are calculated directly by the browser's internal code, rather than JavaScript.

+ +

If, however, you are doing something more complex and involving objects that are not directly accessible inside the DOM (such as 2D Canvas API or WebGL objects), requestAnimationFrame() is the better option in most cases.

+
+ +

How fast does your animation run?

+ +

The smoothness of your animation is directly dependent on your animation's frame rate and it is measured in frames per second (fps). The higher this number is, the smoother your animation will look, to a point.

+ +

Since most screens have a refresh rate of 60Hz, the fastest frame rate you can aim for is 60 frames per second (FPS) when working with web browsers. However, more frames means more processing, which can often cause stuttering and skipping — also known as dropping frames, or jank.

+ +

If you have a monitor with a 60Hz refresh rate and you want to achieve 60 FPS you have about 16.7 milliseconds (1000 / 60) to execute your animation code to render each frame. This is a reminder that you'll need to be mindful of the amount of code that you try to run during each pass through the animation loop.

+ +

requestAnimationFrame() always tries to get as close to this magic 60 FPS value as possible. Sometimes, it isn't possible — if you have a really complex animation and you are running it on a slow computer, your frame rate will be less. In all cases, requestAnimationFrame() will always do the best it can with what it has available.

+ +

How does requestAnimationFrame() differ from setInterval() and setTimeout()?

+ +

Let's talk a little bit more about how the requestAnimationFrame() method differs from the other methods used earlier. Looking at our code from above:

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

Let's now see how to do the same thing using setInterval():

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

As we covered earlier, you don't specify a time interval for requestAnimationFrame(). It just runs it as quickly and smoothly as possible in the current conditions. The browser also doesn't waste time running it if the animation is offscreen for some reason, etc.

+ +

setInterval(), on the other hand requires an interval to be specified. We arrived at our final value of 17 via the formula 1000 milliseconds / 60Hz, and then rounded it up. Rounding up is a good idea; if you rounded down, the browser might try to run the animation faster than 60 FPS, and it wouldn't make any difference to the animation's smoothness, anyway. As we said before, 60Hz is the standard refresh rate.

+ +

Including a timestamp

+ +

The actual callback passed to the requestAnimationFrame() function can be given a parameter, too: a timestamp value, that represents the time since the requestAnimationFrame() started running.

+ +

This is useful as it allows you to run things at specific times and at a constant pace, regardless of how fast or slow your device might be. The general pattern you'd use looks something like this:

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

Browser support

+ +

requestAnimationFrame() is supported in more recent browsers than setInterval()/setTimeout().  Interestingly, it is available in Internet Explorer 10 and above.

+ +

So, unless you need to support older versions of IE, there is little reason to not use requestAnimationFrame().

+ +

A simple example

+ +

Enough with the theory! Let's build your own personal requestAnimationFrame() example. You're going to create a simple "spinner animation"—the kind you might see displayed in an app when it is busy connecting to the server, etc.

+ +
+

Note: In a real world example, you should probably use CSS animations to run this kind of simple animation. However, this kind of example is very useful to demonstrate requestAnimationFrame() usage, and you'd be more likely to use this kind of technique when doing something more complex such as updating the display of a game on each frame.

+
+ +
    +
  1. +

    Grab a basic HTML template (such as this one).

    +
  2. +
  3. +

    Put an empty {{htmlelement("div")}} element inside the {{htmlelement("body")}}, then add a ↻ character inside it. This circular arrow character will act as our spinner for this example.

    +
  4. +
  5. +

    Apply the following CSS to the HTML template (in whatever way you prefer). This sets a red background on the page, sets the <body> height to 100% of the {{htmlelement("html")}} height, and centers the <div> inside the <body>, horizontally and vertically.

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

    Insert a {{htmlelement("script")}} element just above the closing </body> tag.

    +
  8. +
  9. +

    Insert the following JavaScript inside your <script> element. Here, you're storing a reference to the <div> inside a constant, setting a rotateCount variable to 0, setting an uninitialized variable that will later be used to contain a reference to the requestAnimationFrame() call, and setting a startTime variable to null, which will later be used to store the start time of the requestAnimationFrame().

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

    Below the previous code, insert a draw() function that will be used to contain our animation code, which includes the timestamp parameter:

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

    Inside draw(), add the following lines. They will define the start time if it is not defined already (this will only happen on the first loop iteration), and set the rotateCount to a value to rotate the spinner by (the current timestamp, take the starting timestamp, divided by three so it doesn't go too fast):

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

    Below the previous line inside draw(), add the following block — this ensures that the value of rotateCount is between 0 and 359, by setting the value to its modulo of 360 (i.e. the remainder left over when the value is divided by 360) — so the circle animation can continue uninterrupted, at a sensible, low value. Note that this isn't strictly necessary, but it is easier to work with values of 0359 degrees than values like "128000 degrees".

    + +
      rotateCount %= 360; 
    +
  16. +
  17. Next, below the previous block add the following line to actually rotate the spinner: +
    spinner.style.transform = `rotate(${rotateCount}deg)`;
    +
  18. +
  19. +

    At the very bottom inside the draw() function, insert the following line. This is the key to the whole operation — you are setting the variable defined earlier to an active requestAnimation() call, which takes the draw() function as its parameter. This starts the animation off, constantly running the draw() function at a rate as near 60 FPS as possible.

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

    Below the draw() function definition, add a call to the draw() function to start the animation.

    + +
    draw();
    +
  22. +
+ +
+

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

+
+ +

Clearing a requestAnimationFrame() call

+ +

Clearing a requestAnimationFrame() call can be done by calling the corresponding cancelAnimationFrame() method. (Note that the function name starts with "cancel", not "clear" as with the "set..." methods.) 

+ +

Just pass it the value returned by the requestAnimationFrame() call to cancel, which you stored in the variable rAF:

+ +
cancelAnimationFrame(rAF);
+ +

Active learning: Starting and stopping our spinner

+ +

In this exercise, we'd like you to test out the cancelAnimationFrame() method by taking our previous example and updating it, adding an event listener to start and stop the spinner when the mouse is clicked anywhere on the page.

+ +

Some hints:

+ + + +
+

Note: Try this yourself first; if you get really stuck, check out of our live example and source code.

+
+ +

Throttling a requestAnimationFrame() animation

+ +

One limitation of requestAnimationFrame() is that you can't choose your frame rate. This isn't a problem most of the time, as generally you want your animation to run as smoothly as possible. But what about when you want to create an old school, 8-bit-style animation?

+ +

This was a problem, for example, in the Monkey Island-inspired walking animation from our Drawing Graphics article:

+ +

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

+ +

In this example, you have to animate both the position of the character on the screen, and the sprite being shown. There are only 6 frames in the sprite's animation. If you showed a different sprite frame for every frame displayed on the screen by requestAnimationFrame(), Guybrush would move his limbs too fast and the animation would look ridiculous. This example therefore throttles the rate at which the sprite cycles its frames using the following code:

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

So the code only cycles the sprite once every 13 animation frames.

+ +

...Actually, it's about every 6.5 frames, as we update posX (character's position on the screen) by two each frame:

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

This is the code that calculates how to update the position in each animation frame.

+ +

The method you use to throttle your animation will depend on your particular code. For instance, in the earlier spinner example, you could make it appear to move slower by only increasing rotateCount by one on each frame, instead of two.

+ +

Active learning: a reaction game

+ +

For the final section of this article, you'll create a 2-player reaction game. The game will have two players, one of whom controls the game using the A key, and the other with the L key.

+ +

When the Start button is pressed, a spinner like the one we saw earlier is displayed for a random amount of time between 5 and 10 seconds. After that time, a message will appear saying "PLAYERS GO!!" — once this happens, the first player to press their control button will win the game.

+ +

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

+ +

Let's work through this:

+ +
    +
  1. +

    First of all, download the starter file for the app. This contains the finished HTML structure and CSS styling, giving us a game board that shows the two players' information (as seen above), but with the spinner and results paragraph displayed on top of one another. You just have to write the JavaScript code.

    +
  2. +
  3. +

    Inside the empty {{htmlelement("script")}} element on your page, start by adding the following lines of code that define some constants and variables you'll need in the rest of the code:

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

    In order, these are:

    + +
      +
    1. A reference to the spinner, so you can animate it.
    2. +
    3. A reference to the {{htmlelement("div")}} element that contains the spinner, used for showing and hiding it.
    4. +
    5. A rotate count. This determines how much you want to show the spinner rotated on each frame of the animation.
    6. +
    7. A null start time. This will be populated with a start time when the spinner starts spinning.
    8. +
    9. An uninitialized variable to later store the {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} call that animates the spinner.
    10. +
    11. A reference to the Start button.
    12. +
    13. A reference to the results paragraph.
    14. +
    +
  4. +
  5. +

    Next, below the previous lines of code, add the following function. It takes two numbers and returns a random number between the two. You'll need this to generate a random timeout interval later on.

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

    Next add  the draw() function, which animates the spinner. This is very similar to the version from the simple spinner example, earlier:

    + +
    function draw(timestamp) {
    +  if(!startTime) {
    +   startTime = timestamp;
    +  }
    +
    +  rotateCount = (timestamp - startTime) / 3;
    +
    +  rotateCount %= 360;
    +  
    +  spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
    +  rAF = requestAnimationFrame(draw);
    +}
    +
  8. +
  9. +

    Now it is time to set up the initial state of the app when the page first loads. Add the following two lines, which hide the results paragraph and spinner container using display: none;.

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

    Next, define a reset() function, which sets the app back to the original state required to start the game again after it has been played. Add the following at the bottom of your code:

    + +
    function reset() {
    +  btn.style.display = 'block';
    +  result.textContent = '';
    +  result.style.display = 'none';
    +}
    +
  12. +
  13. +

    Okay, enough preparation!  It's time to make the game playable! Add the following block to your code. The start() function calls draw() to start the spinner spinning and display it in the UI, hides the Start button so you can't mess up the game by starting it multiple times concurrently, and runs a setTimeout() call that runs a setEndgame() function after a random interval between 5 and 10 seconds has passed. The following block also adds an event listener to your button to run the start() function when it is clicked.

    + +
    btn.addEventListener('click', start);
    +
    +function start() {
    +  draw();
    +  spinnerContainer.style.display = 'block';
    +  btn.style.display = 'none';
    +  setTimeout(setEndgame, random(5000,10000));
    +}
    + +
    +

    Note: You'll see this example is calling setTimeout() without storing the return value. (So, not let myTimeout = setTimeout(functionName, interval).) 

    + +

    This works just fine, as long as you don't need to clear your interval/timeout at any point. If you do, you'll need to save the returned identifier!

    +
    + +

    The net result of the previous code is that when the Start button is pressed, the spinner is shown and the players are made to wait a random amount of time before they are asked to press their button. This last part is handled by the setEndgame() function, which you'll define next.

    +
  14. +
  15. +

    Add the following function to your code next:

    + +
    function setEndgame() {
    +  cancelAnimationFrame(rAF);
    +  spinnerContainer.style.display = 'none';
    +  result.style.display = 'block';
    +  result.textContent = 'PLAYERS GO!!';
    +
    +  document.addEventListener('keydown', keyHandler);
    +
    +  function keyHandler(e) {
    +    let isOver = false;
    +    console.log(e.key);
    +
    +    if (e.key === "a") {
    +      result.textContent = 'Player 1 won!!';
    +      isOver = true;
    +    } else if (e.key === "l") {
    +      result.textContent = 'Player 2 won!!';
    +      isOver = true;
    +    }
    +
    +    if (isOver) {
    +      document.removeEventListener('keydown', keyHandler);
    +      setTimeout(reset, 5000);
    +    }
    +  };
    +}
    + +

    Stepping through this:

    + +
      +
    1. First, cancel the spinner animation with {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}} (it is always good to clean up unneeded processes), and hide the spinner container.
    2. +
    3. Next, display the results paragraph and set its text content to "PLAYERS GO!!" to signal to the players that they can now press their button to win.
    4. +
    5. Attach a keydown event listener to the document. When any button is pressed down, the keyHandler() function is run.
    6. +
    7. Inside keyHandler(), the code includes the event object as a parameter (represented by e) — its {{domxref("KeyboardEvent.key", "key")}} property contains the key that was just pressed, and you can use this to respond to specific key presses with specific actions.
    8. +
    9. Set the variable isOver to false, so we can track whether the correct keys were pressed for player 1 or 2 to win. We don't want the game ending when a wrong key was pressed.
    10. +
    11. Log e.key to the console, which is a useful way of finding out the key value of different keys you are pressing.
    12. +
    13. When e.key is "a", display a message to say that Player 1 won, and when e.key is "l", display a message to say Player 2 won. (Note: This will only work with lowercase a and l — if an uppercase A or L is submitted (the key plus Shift), it is counted as a different key!) If one of these keys was pressed, set isOver to true.
    14. +
    15. Only if isOver is true, remove the keydown event listener using {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} so that once the winning press has happened, no more keyboard input is possible to mess up the final game result. You also use setTimeout() to call reset() after 5 seconds — as explained earlier, this function resets the game back to its original state so that a new game can be started.
    16. +
    +
  16. +
+ +

That's it—you're all done!

+ +
+

Note: If you get stuck, check out our version of the reaction game (see the source code also).

+
+ +

Conclusion

+ +

So that's it — all the essentials of async loops and intervals covered in one article. You'll find these methods useful in a lot of situations, but take care not to overuse them! Because they still run on the main thread, heavy and intensive callbacks (especially those that manipulate the DOM) can really slow down a page if you're not careful.

+ +

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

+ +

In this module

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