在这个模块中,我们来看看异步JavaScript,为什么它很重要,以及如何使用它来有效地处理潜在的阻塞操作,比如从服务器获取资源。
指南
异步JavaScript介绍
在本文中,我们将学习同步(synchronous
)和异步编程(asynchronous
),为什么我们经常需要使用异步技术,以及与JavaScript中异步函数的历史实现方式相关的问题。
如何使用promises
这里我们将介绍promises ,并展示如何使用基于promises 的api。我们还将介绍async
和await
关键字。
实现基于promise的API
本文将概述如何实现您自己的基于承诺的API。
引入 workers
工作线程(workers
)使您能够在单独的线程中运行某些任务,以保持主代码的响应性。在本文中,我们将重写一个长时间运行的同步函数以使用worker。
评估
测序动画
评估要求你使用promise以特定的顺序播放一组动画。
See also
来自Marijn Haverbeke的精彩的Eloquent JavaScript在线书中的异步编程。
1、异步JavaScript介绍
在本文中,我们将解释什么是异步编程,为什么需要异步编程,并简要讨论JavaScript中异步函数的一些历史实现方式。
异步编程
是一种技术,它使您的程序能够启动一个可能长期运行的任务
,并且在该任务运行时仍然能够响应其他事件
,而不必等到该任务完成。一旦该任务完成,您的程序就会显示结果
。
浏览器提供的许多函数,尤其是最有趣的函数,可能会花费很长时间,因此是异步的。例如:
- 使用fetch()发出HTTP请求
- 使用getUserMedia()访问用户的相机或麦克风
- 使用showOpenFilePicker()要求用户选择文件
因此,尽管您可能不需要经常实现自己的异步函数,但您很可能需要正确地使用它们。
在本文中,我们将首先研究长时间运行的同步函数的问题,这使得异步编程成为必要。
1.1 同步编程模式
考虑下面的代码:
const name = "Miriam";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Miriam!"
This code:
- 声明一个名为
name
的字符串。 - 声明另一个名为
greeting
的字符串,它使用name
。 - 向JavaScript控制台输出问候语。
我们应该注意到,浏览器实际上是按照我们编写程序的顺序,一行一行地执行程序。在每个点上,浏览器等待该行完成其工作,然后再进入下一行。它必须这样做,因为每一行都依赖于前一行所做的工作。
这是一个同步程序(synchronous program
)。即使我们调用一个单独的函数,它仍然是同步的,像这样:
function makeGreeting(name) {
return `Hello, my name is ${name}!`;
}
const name = "Miriam";
const greeting = makeGreeting(name);
console.log(greeting);
// "Hello, my name is Miriam!"
在这里,makeGreeting()
是一个同步函数(synchronous function
),因为调用者必须等待函数完成它的工作并返回一个值,然后调用者才能继续。
长时间运行的同步函数
如果同步函数需要很长时间怎么办?
当用户点击“Generate primes”按钮时,下面的程序使用一个非常低效的算法来生成多个大质数。用户指定的质数越多,操作所需的时间就越长。
<label for="quota">Number of primes:</label>
<input type="text" id="quota" name="quota" value="1000000" />
<button id="generate">Generate primes</button>
<button id="reload">Reload</button>
<div id="output"></div>
const MAX_PRIME = 1000000;
function isPrime(n) {
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) {
return false;
}
}
return n > 1;
}
const random = (max) => Math.floor(Math.random() * max);
function generatePrimes(quota) {
const primes = [];
while (primes.length < quota) {
const candidate = random(MAX_PRIME);
if (isPrime(candidate)) {
primes.push(candidate);
}
}
return primes;
}
const quota = document.querySelector("#quota");
const output = document.querySelector("#output");
document.querySelector("#generate").addEventListener("click", () => {
const primes = generatePrimes(quota.value);
output.textContent = `Finished generating ${quota.value} primes!`;
});
document.querySelector("#reload").addEventListener("click", () => {
document.location.reload();
});
尝试点击“Generate primes”。这取决于你的电脑有多快,在程序显示“完成”消息之前可能需要几秒钟的时间。
长时间运行的同步函数的问题
下一个示例与上一个示例类似,只是我们添加了一个文本框供您输入。这一次,点击“生成质数(Generate primes)”,然后尝试在文本框中立即输入。
您会发现,当我们的generatePrimes()
函数运行时,我们的程序完全没有响应:您不能键入任何内容、单击任何内容或执行任何其他操作。
这是长时间运行的同步函数的基本问题。我们需要的是一种方法,让我们的程序:
- 通过调用函数启动长时间运行的操作。
- 让该函数开始操作并立即返回,这样我们的程序仍然可以响应其他事件。
- 当操作最终完成时,通知我们操作的结果。
这正是异步函数所能做的。本模块的其余部分将解释如何在JavaScript中实现它们。
1.2 事件处理器
我们刚刚看到的异步函数的描述可能会让您想起事件处理程序,如果确实如此,那么您是对的。事件处理程序实际上是异步编程的一种形式:您提供一个将被调用的函数(事件处理程序),不是立即调用,而是在事件发生时调用。如果“事件”是“异步操作已经完成”,那么该事件可以用来通知调用者异步函数调用的结果。
一些早期的异步APIs 就是以这种方式使用事件的。XMLHttpRequest API使您能够使用JavaScript向远程服务器发出HTTP请求。由于这可能需要很长时间,所以它是一个异步APIx,通过将事件侦听器附加到XMLHttpRequest
对象,您可以获得有关请求的进度和最终完成的通知。
下面的示例演示了实际操作。点击“Click to start request”发送请求。我们创建一个新的XMLHttpRequest
并监听它的loadend事件。处理程序将“Finished!”消息连同状态码一起记录下来。
在添加事件侦听器之后,我们发送请求。注意,在此之后,我们可以记录“已启动的XHR请求”:也就是说,当请求正在进行时,我们的程序可以继续运行,当请求完成时,我们的事件处理程序将被调用。
<button id="xhr">Click to start request</button>
<button id="reload">Reload</button>
<pre readonly class="event-log"></pre>
const log = document.querySelector(".event-log");
document.querySelector("#xhr").addEventListener("click", () => {
log.textContent = "";
const xhr = new XMLHttpRequest();
xhr.addEventListener("loadend", () => {
log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
});
xhr.open(
"GET",
"https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json",
);
xhr.send();
log.textContent = `${log.textContent}Started XHR request\n`;
});
document.querySelector("#reload").addEventListener("click", () => {
log.textContent = "";
document.location.reload();
});
这就像我们在前面的模块中遇到的事件处理程序一样,除了用户操作(例如用户单击按钮)的事件,事件是某个对象状态的变化。
1.3 回调函数
事件处理程序是一种特殊类型的回调。回调只是将一个函数传递给另一个函数,并期望回调将在适当的时间被调用
。正如我们刚才看到的,回调曾经是JavaScript中实现异步函数的主要方式。
然而,当回调本身必须调用接受回调的函数时,基于回调的代码可能很难理解。如果需要执行一些分解为一系列异步函数的操作,这是一种常见的情况。例如,考虑以下内容:
function doStep1(init) {
return init + 1;
}
function doStep2(init) {
return init + 2;
}
function doStep3(init) {
return init + 3;
}
function doOperation() {
let result = 0;
result = doStep1(result);
result = doStep2(result);
result = doStep3(result);
console.log(`result: ${result}`);
}
doOperation();
这里我们有一个被分成三步的操作,每一步都依赖于最后一步。在我们的示例中,第一步为输入加1,第二步加2,第三步加3。从输入0开始,最终结果是6(0 + 1 + 2 + 3)。作为一个同步程序,这是非常简单的。但是,如果我们使用回调实现这些步骤呢?
function doStep1(init, callback) {
const result = init + 1;
callback(result);
}
function doStep2(init, callback) {
const result = init + 2;
callback(result);
}
function doStep3(init, callback) {
const result = init + 3;
callback(result);
}
function doOperation() {
doStep1(0, (result1) => {
doStep2(result1, (result2) => {
doStep3(result2, (result3) => {
console.log(`result: ${result3}`);
});
});
});
}
doOperation();
因为我们必须在回调函数内部调用回调函数,所以我们得到了一个嵌套很深的doOperation()
函数,这更难阅读和调试。这有时被称为“回调地狱(callback hell
)”或“厄运金字塔(pyramid of doom)”(因为缩进看起来像一个侧面的金字塔)。
当我们像这样嵌套回调时,处理错误也会变得非常困难:通常你必须在“金字塔”的每一层处理错误,而不是只在顶层处理一次错误。
由于这些原因,大多数现代异步 API 不使用回调。相反,JavaScript中异步编程的基础是Promise,这是下一篇文章的主题。
2、如何使用 promises
Promises 是现代JavaScript异步编程的基础。promise是异步函数返回的对象,它表示操作的当前状态。当promise返回给调用者时,操作通常还没有完成,但是 promise 对象提供了处理操作最终成功或失败的方法。
在上一篇文章中,我们讨论了使用回调来实现异步函数。通过这种设计,您调用异步函数,并传入回调函数。函数立即返回,并在操作完成时调用回调函数。
使用基于Promise的API,异步函数启动操作并返回Promise对象。然后,您可以将处理程序附加到此承诺对象,这些处理程序将在操作成功或失败时执行。
2.1 使用fetch() API
在本例中,我们将从https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json下载JSON文件,并记录有关它的一些信息。
为此,我们将向服务器发出一个HTTP请求。在HTTP请求中,我们向远程服务器发送请求消息,远程服务器向我们返回响应。在本例中,我们将发送请求以从服务器获取JSON文件。还记得在上一篇文章中,我们使用XMLHttpRequest
API发出HTTP请求的地方吗?在本文中,我们将使用fetch()
API,它是XMLHttpRequest
的现代的、基于承诺的替代品。
将此复制到浏览器的JavaScript控制台:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
console.log(fetchPromise);
fetchPromise.then((response) => {
console.log(`Received response: ${response.status}`);
});
console.log("Started request…");
我们在这:
- 调用
fetch()
API,并将返回值赋给fetchPromise
变量 - 之后立即记录
fetchPromise
变量。这应该输出类似于:Promise { <state>: "pending" }
,告诉我们我们有一个Promise对象,它有一个值为"pending"的状态。“pending”状态意味着获取操作仍在进行中。 - 将一个处理函数传递给
Promise
的then()
方法。当(如果)获取操作成功时,promise将调用我们的处理程序,传入一个Response对象,其中包含服务器的响应。 - 记录我们已经启动请求的消息。
Promise { <state>: "pending" }
Started request…
Received response: 200
注意,在我们收到响应之前,已记录了Started request…
。与同步函数不同,fetch()
在请求仍在进行时返回,使我们的程序保持响应。响应显示200
(OK)状态码,表示请求成功。
这看起来很像上一篇文章中的示例,我们在其中向XMLHttpRequest
对象添加了事件处理程序。相反,我们将一个处理程序传递给返回的承诺的then()方法。
2.2 链式 promises
使用fetch()
API,一旦获得了Response
对象,就需要调用另一个函数来获取响应数据。在本例中,我们希望以JSON的形式获取响应数据,因此我们将调用Response
对象的json()
方法。事实证明json()
也是异步的。在这种情况下,我们必须调用两个连续的异步函数。
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise.then((response) => {
const jsonPromise = response.json();
jsonPromise.then((data) => {
console.log(data[0].name);
});
});
在这个例子中,和前面一样,我们给fetch()
返回的promise 添加了一个then()
处理程序。但这一次,我们的处理程序调用response.json()
,然后将一个新的then(
)处理程序传递给response.json()
返回的promise 。
这应该记录“baked beans”(“products.json”中列出的第一个产品的名称)。
但是等等!还记得上一篇文章吗?我们说过,通过在另一个回调中调用一个回调,我们得到了更多嵌套的代码层。我们说过这个“回调地狱”让我们的代码难以理解?这不是一样的,只是有then()
调用吗?
当然是这样。但是promise的优雅特性是then()
本身返回一个promise,该promise将通过传递给它的函数的结果来完成。这意味着我们可以(当然也应该)这样重写上面的代码:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data[0].name);
});
我们不是在第一个then()
的处理程序中调用第二个then()
,而是 调用 json()
返回一个 promise,并在返回值调用第二个then()
。这被称为 promise chaining (链式 Promise),这意味着当我们需要进行连续的异步函数调用时,我们可以避免不断增加的缩进级别。
在进入下一步之前,还有一个部分需要添加。在尝试读取请求之前,我们需要检查服务器是否接受并能够处理请求。我们会检查响应中的状态码,如果不是"OK"就抛出一个错误:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
});
2.3 捕获错误
这将我们带到了最后一部分:我们如何处理错误?fetch()
API可能会因多种原因抛出错误(例如,因为没有网络连接或URL在某种程度上是错误的),如果服务器返回错误,我们自己也会抛出错误。
在上一篇文章中,我们看到嵌套回调的错误处理非常困难,这使得我们在每个嵌套级别都要处理错误。
为了支持错误处理,Promise对象提供了catch()
方法。这很像then()
:调用它并传入一个处理程序函数。然而,传递给then()
的处理程序在异步操作成功时被调用,而传递给catch()
的处理程序在异步操作失败时被调用。
如果将catch()
添加到Promise链的末尾,那么当任何异步函数调用失败时都会调用它。因此,您可以将一个操作实现为几个连续的异步函数调用,并有一个地方来处理所有错误。
试试这个版本的fetch()
代码。我们使用catch()
添加了一个错误处理程序,并修改了URL,使请求失败。
const fetchPromise = fetch(
"bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
})
.catch((error) => {
console.error(`Could not get products: ${error}`);
});
尝试运行这个版本:你应该会看到catch()
处理程序记录的错误。
2.4 async and await
async关键字提供了一种更简单的方法来处理基于异步promise的代码。在函数的开头添加async
使其成为 async 函数:
async function myFunction() {
// This is an async function
}
在async
函数中,您可以在调用返回promise的函数之前使用await
关键字。这使得代码在该点等待,直到promise被兑现,此时promise的兑现值被视为返回值,或者被拒绝的值被抛出。
这使您能够编写使用异步函数但看起来像同步代码的代码。例如,我们可以用它来重写我们的fetch示例:
async function fetchProducts() {
try {
// after this line, our function will wait for the `fetch()` call to be settled
// the `fetch()` call will either return a Response or throw an error
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
// after this line, our function will wait for the `response.json()` call to be settled
// the `response.json()` call will either return the parsed JSON object or throw an error
const data = await response.json();
console.log(data[0].name);
} catch (error) {
console.error(`Could not get products: ${error}`);
}
}
fetchProducts();
这里,我们调用await fetch()
,而不是获得Promise
,我们的调用者返回一个完全完整的Response
对象,就像fetch()
是一个同步函数一样!
我们甚至可以try...catch
块用于错误处理,就像代码是同步的一样。
请注意,async
函数总是返回一个promise,所以你不能这样做:
async function fetchProducts() {
try {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Could not get products: ${error}`);
}
}
const promise = fetchProducts();
console.log(promise[0].name); // "promise" is a Promise object, so this will not work
相反,你需要做这样的事情:
async function fetchProducts() {
try {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Could not get products: ${error}`);
}
}
const promise = fetchProducts();
promise.then((data) => console.log(data[0].name));
另外,请注意,您只能在async
函数中使用await
,除非您的代码位于JavaScript模块中。这意味着你不能在一个正常的脚本中这样做:
try {
// using await outside an async function is only allowed in a module
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data[0].name);
} catch (error) {
console.error(`Could not get products: ${error}`);
}
你可能会经常在使用promise 链的地方使用async
函数,它们让使用promise 变得更加直观。
请记住,就像promise 链一样,await
强制异步操作串联完成。如果下一个操作的结果依赖于上一个操作的结果,这是必要的,但如果不是这种情况,那么像Promise.all()
这样的东西会更高效。
许多现代Web API都是基于promise的,包括WebRTC、Web Audio API、Media Capture和Streams API等等。
3、如何实现基于 promise 的API
通常,在实现基于promise的API时,您将封装一个异步操作,该操作可能使用事件、普通回调或消息传递模型。您将安排一个Promise
对象来正确地处理该操作的成功或失败。
3.1 实现alarm() API
在本例中,我们将实现一个基于promise的警报API,称为alarm()
。它将接受要唤醒的人的名字和唤醒该人之前等待的以毫秒为单位的延迟作为参数。延迟后,该函数将发送“Wake up!”消息,包括我们需要唤醒的人的名字。
封装 setTimeout ()
我们将使用setTimeout()
API来实现alarm()
函数。setTimeout()
API接受一个回调函数和一个以毫秒为单位的延迟作为参数。当调用setTimeout()
时,它启动一个设置为给定延迟的计时器,当时间到期时,它调用给定的函数。
在下面的例子中,我们用一个回调函数和一个1000毫秒的延迟来调用setTimeout()
:
<button id="set-alarm">Set alarm</button>
<div id="output"></div>
const output = document.querySelector("#output");
const button = document.querySelector("#set-alarm");
function setAlarm() {
setTimeout(() => {
output.textContent = "Wake up!";
}, 1000);
}
button.addEventListener("click", setAlarm);
Promise()构造函数
我们的alarm()
函数将在计时器到期时返回一个Promise
。它将向then()
处理程序传递一个“Wake up!”消息,如果调用者提供负延迟值,它将拒绝Promise。
这里的关键组件是Promise()
构造函数。Promise()
构造函数接受一个函数作为参数。我们称这个函数为executor。当你创建一个新的Promise时,你提供了executor的实现。
这个执行器函数本身有两个参数,它们都是函数,通常称为resolve
和reject
。在执行器实现中,调用底层异步函数。如果异步函数成功,则调用resolve
,如果失败,则调用rejec
t。如果执行器函数抛出错误,则自动调用reject
。您可以将任何类型的单个参数传递给resolve
和reject
。
所以我们可以这样实现alarm()
:
function alarm(person, delay) {
return new Promise((resolve, reject) => {
if (delay < 0) {
throw new Error("Alarm delay must not be negative");
}
setTimeout(() => {
resolve(`Wake up, ${person}!`);
}, delay);
});
}
这个函数创建并返回一个新的Promise
。在promise的执行器中,我们:
- 检查
delay
是否为负,如果为负则抛出错误。 - 调用
setTimeout()
,传递回调和delay
。回调将在计时器到期时被调用,在回调中我们调用resolve
,传递我们的"Wake up!"
消息。
3.2 使用alarm() API
这部分应该与上一篇文章非常熟悉。我们可以调用alarm()
,然后在返回的promise
上调用then()
和catch()
来设置promise
兑现和拒绝的处理程序。
const name = document.querySelector("#name");
const delay = document.querySelector("#delay");
const button = document.querySelector("#set-alarm");
const output = document.querySelector("#output");
function alarm(person, delay) {
return new Promise((resolve, reject) => {
if (delay < 0) {
throw new Error("Alarm delay must not be negative");
}
setTimeout(() => {
resolve(`Wake up, ${person}!`);
}, delay);
});
}
button.addEventListener("click", () => {
alarm(name.value, delay.value)
.then((message) => (output.textContent = message))
.catch((error) => (output.textContent = `Couldn't set alarm: ${error}`));
});
用 async
和await
使用alarm()
API
由于alarm()
返回一个Promise
,我们可以用它做任何其他Promise
可以做的事情:Promise 链, Promise.all()
和async / await
:
const name = document.querySelector("#name");
const delay = document.querySelector("#delay");
const button = document.querySelector("#set-alarm");
const output = document.querySelector("#output");
function alarm(person, delay) {
return new Promise((resolve, reject) => {
if (delay < 0) {
throw new Error("Alarm delay must not be negative");
}
setTimeout(() => {
resolve(`Wake up, ${person}!`);
}, delay);
});
}
button.addEventListener("click", async () => {
try {
const message = await alarm(name.value, delay.value);
output.textContent = message;
} catch (error) {
output.textContent = `Couldn't set alarm: ${error}`;
}
});
4、workers 简介
在“异步JavaScript”模块的最后一篇文章中,我们将介绍workers
,它使您能够在单独的执行线程中运行一些任务。
在本模块的第一篇文章中,我们看到了当您的程序中有一个长时间运行的同步任务时会发生什么—整个窗口变得完全没有响应。从根本上说,这样做的原因是程序是单线程(single-threaded
)的。线程(thread
)是程序所遵循的指令序列。因为程序由一个线程组成,它一次只能做一件事:所以如果它在等待长时间运行的同步调用返回,它就不能做任何其他事情。
Workers
使您能够在不同的线程中运行一些任务,因此您可以启动任务,然后继续进行其他处理(例如处理用户操作)。
所有这一切的一个问题是
,如果多个线程可以访问相同的共享数据,它们就有可能独立地、意外地(相对于彼此)更改数据。这可能会导致难以发现的bug。
为了在网络上避免这些问题,你的主代码和工作代码永远不能直接访问彼此的变量,只有在非常特殊的情况下才能真正“共享”数据。worker和main代码在完全独立的世界中运行,并且只通过相互发送消息进行交互。特别是,这意味着工作者不能访问DOM(window, document, page elements, and so on)。
有三种不同类型的workers:
- dedicated workers
- shared workers
- service workers
在本文中,我们将介绍第一种 worke r的一个示例,然后简要讨论其他两种。
4.1 Using web workers
还记得在第一篇文章中,我们有一个计算质数的页面吗?我们将使用一个worker来运行素数计算,这样我们的页面就可以对用户操作保持响应。
同步质数生成器
让我们先看一下前面例子中的JavaScript:
function generatePrimes(quota) {
function isPrime(n) {
for (let c = 2; c <= Math.sqrt(n); ++c) {
if (n % c === 0) {
return false;
}
}
return true;
}
const primes = [];
const maximum = 1000000;
while (primes.length < quota) {
const candidate = Math.floor(Math.random() * (maximum + 1));
if (isPrime(candidate)) {
primes.push(candidate);
}
}
return primes;
}
document.querySelector("#generate").addEventListener("click", () => {
const quota = document.querySelector("#quota").value;
const primes = generatePrimes(quota);
document.querySelector(
"#output",
).textContent = `Finished generating ${quota} primes!`;
});
document.querySelector("#reload").addEventListener("click", () => {
document.querySelector("#user-input").value =
'Try typing in here immediately after pressing "Generate primes"';
document.location.reload();
});
在这个程序中,在调用generateprime()
之后,程序变得完全没有响应。
用 worker 进行质数生成
对于本例,首先在https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start上创建文件的本地副本。这个目录下有四个文件:
- index.html
- style.css
- main.js
- generate.js
“index.html
”文件和“style.css
”文件已经完成:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Prime numbers</title>
<script src="main.js" defer></script>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<label for="quota">Number of primes:</label>
<input type="text" id="quota" name="quota" value="1000000" />
<button id="generate">Generate primes</button>
<button id="reload">Reload</button>
<textarea id="user-input" rows="5" cols="62">
Try typing in here immediately after pressing "Generate primes"
</textarea>
<div id="output"></div>
</body>
</html>
"main.js"和"generate.js"文件为空。我们将把主代码添加到"main.js"中,把 worker 代码添加到"generate.js"中。
现在将以下代码复制到"main.js"中:
// Create a new worker, giving it the code in "generate.js"
const worker = new Worker("./generate.js");
// When the user clicks "Generate primes", send a message to the worker.
// The message command is "generate", and the message also contains "quota",
// which is the number of primes to generate.
document.querySelector("#generate").addEventListener("click", () => {
const quota = document.querySelector("#quota").value;
worker.postMessage({
command: "generate",
quota,
});
});
// When the worker sends a message back to the main thread,
// update the output box with a message for the user, including the number of
// primes that were generated, taken from the message data.
worker.addEventListener("message", (message) => {
document.querySelector(
"#output",
).textContent = `Finished generating ${message.data} primes!`;
});
document.querySelector("#reload").addEventListener("click", () => {
document.querySelector("#user-input").value =
'Try typing in here immediately after pressing "Generate primes"';
document.location.reload();
});
- 首先,我们使用
Worker()
构造函数创建worker
。我们给它传递一个指向 worker 脚本的URL。一旦创建了worker,就会执行 worker脚本。 - 接下来,与同步版本一样,我们向“Generate primes”按钮添加一个
click
事件处理程序。但是现在,我们不是调用generatePrimes()
函数,而是使用worker postmessage()向worker发送消息。这个消息可以接受一个参数,在这种情况下,我们传递一个包含两个属性的JSON对象:command
: 一个字符串,标识我们想让worker做的事情(以防worker可以做不止一件事)quota
: 生成质数的数量。
- 接下来,我们向 worker 添加一个
message
事件处理程序。这样,worker就可以告诉我们它何时完成,并将结果数据传递给我们。我们的处理程序从消息的data
属性中获取数据,并将其写入输出元素(数据与quota
完全相同,因此这有点没有意义,但它显示了原理)。 - 最后,我们为“Reload”按钮实现
click
事件处理程序。这与同步版本完全相同。
现在来看worker代码。将以下代码复制到"generate.js"中:
// Listen for messages from the main thread.
// If the message command is "generate", call `generatePrimes()`
addEventListener("message", (message) => {
if (message.data.command === "generate") {
generatePrimes(message.data.quota);
}
});
// Generate primes (very inefficiently)
function generatePrimes(quota) {
function isPrime(n) {
for (let c = 2; c <= Math.sqrt(n); ++c) {
if (n % c === 0) {
return false;
}
}
return true;
}
const primes = [];
const maximum = 1000000;
while (primes.length < quota) {
const candidate = Math.floor(Math.random() * (maximum + 1));
if (isPrime(candidate)) {
primes.push(candidate);
}
}
// When we have finished, send a message to the main thread,
// including the number of primes we generated.
postMessage(primes.length);
}
请记住,只要主脚本创建了worker,它就会运行。
worker要做的第一件事是开始监听来自主脚本的消息。它使用addEventListener()
来实现这一点,它是worker中的一个全局函数。在message
事件处理程序中,事件的data
属性包含从主脚本传递的参数的副本。如果主脚本传递了generate
命令,我们调用generatePrimes()
,从message 事件传入quota
。
generatePrimes()
函数就像同步版本一样,不同之处在于,当我们完成时,我们不是返回一个值,而是向主脚本发送一条消息。为此,我们使用postMessage()
函数,它与addEventListener()
一样是worker中的全局函数。正如我们已经看到的,主脚本正在监听此消息,并在接收到消息时更新DOM。
注意:要运行这个站点,你必须运行一个本地web服务器,因为
file://
url不允许加载worker。请参阅关于设置本地测试服务器的指南。完成后,您应该能够点击“Generate primes”,并让您的主页保持响应。
如果您在创建或运行示例时遇到任何问题,您可以查看完成的版本。
4.2 其他类型的workers
我们刚刚创建的worker 被称为 dedicated worker。这意味着它由单个脚本实例使用。
不过,还有其他类型的 workers:
- 共享工作线程可以在不同的窗口中运行的几个不同的脚本。
- Service worker就像代理服务器一样,缓存资源,这样web应用程序就可以在用户离线时工作。它们是渐进式Web应用程序的关键组成部分。
5、序列动画
在此评估中,您将更新页面以按顺序播放一系列动画。为此,您将使用我们在如何使用Promises这篇文章中学到的一些技术。
5.1 Starting point
在https://github.com/mdn/learning-area/tree/main/javascript/asynchronous/sequencing-animations/start上创建文件的本地副本。它包含四个文件:
- alice.svg
- index.html
- main.js
- style.css
你唯一需要编辑的文件是“main.js”。
如果你在浏览器中打开"index.html",你会看到三张对角线排列的图片:
这些图片取自我们的使用Web动画API指南。
5.2 项目简介
我们想要更新这个页面,所以我们对所有三个图像应用动画,一个接一个。所以当第一个完成时,我们给第二个动画动画,当第二个完成时,我们给第三个动画动画。
动画已经在“main.js”中定义:它只是旋转图像并缩小它直到它消失。
为了让您更多地了解我们希望页面如何工作,请查看完成的示例。请注意,动画只运行一次:要再次看到它们,请重新加载页面。
5.3 完成步骤
使第一个图像动画化
我们正在使用Web Animations API来对图像进行动画处理,特别是element.animate()
方法。
更新"main.js"以添加对alice1.animate()
的调用,如下所示:
const aliceTumbling = [
{ transform: "rotate(0) scale(1)" },
{ transform: "rotate(360deg) scale(0)" },
];
const aliceTiming = {
duration: 2000,
iterations: 1,
fill: "forwards",
};
const alice1 = document.querySelector("#alice1");
const alice2 = document.querySelector("#alice2");
const alice3 = document.querySelector("#alice3");
alice1.animate(aliceTumbling, aliceTiming);
重新加载页面,您应该会看到第一个图像旋转和收缩。
设置所有图像的动画
接下来,我们想要在alice1
完成时激活alice2
,在alice2
完成时激活alice3
。
animate()
方法返回一个 Animation对象。这个对象有一个finished
属性,这是一个Promise
,当动画完成播放时被实现。所以我们可以使用这个承诺来知道什么时候开始下一个动画。
我们希望你尝试几种不同的方法来实现它,以加强使用Promise的不同方式。
- 首先,实现一些可以工作的东西,但存在我们在使用回调的讨论中看到的“回调地狱”问题的Promise版本。
- 接下来,将其实现为 promise chain。请注意,由于箭头函数可以使用不同的形式,因此可以使用几种不同的方法来编写它。尝试一些不同的形式。哪个最简洁?你觉得哪一本最易读?
- 最后,使用
async
和await
来实现它。
记住,element.animate()
不返回一个Promise
:它返回一个带有finished
属性的Animation
对象,该属性是一个Promise
。