js Learn(异步JavaScript)

news2024/11/17 19:42:40

在这个模块中,我们来看看异步JavaScript,为什么它很重要,以及如何使用它来有效地处理潜在的阻塞操作,比如从服务器获取资源。

指南

异步JavaScript介绍

在本文中,我们将学习同步(synchronous)和异步编程(asynchronous),为什么我们经常需要使用异步技术,以及与JavaScript中异步函数的历史实现方式相关的问题。

如何使用promises

这里我们将介绍promises ,并展示如何使用基于promises 的api。我们还将介绍asyncawait关键字。

实现基于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:

  1. 声明一个名为name的字符串。
  2. 声明另一个名为greeting的字符串,它使用name
  3. 向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()函数运行时,我们的程序完全没有响应:您不能键入任何内容、单击任何内容或执行任何其他操作。
在这里插入图片描述
这是长时间运行的同步函数的基本问题。我们需要的是一种方法,让我们的程序:

  1. 通过调用函数启动长时间运行的操作。
  2. 让该函数开始操作并立即返回,这样我们的程序仍然可以响应其他事件。
  3. 当操作最终完成时,通知我们操作的结果。

这正是异步函数所能做的。本模块的其余部分将解释如何在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…");

我们在这:

  1. 调用fetch() API,并将返回值赋给fetchPromise变量
  2. 之后立即记录fetchPromise变量。这应该输出类似于:Promise { <state>: "pending" },告诉我们我们有一个Promise对象,它有一个值为"pending"的状态。“pending”状态意味着获取操作仍在进行中。
  3. 将一个处理函数传递给Promisethen()方法。当(如果)获取操作成功时,promise将调用我们的处理程序,传入一个Response对象,其中包含服务器的响应。
  4. 记录我们已经启动请求的消息。
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的实现。

这个执行器函数本身有两个参数,它们都是函数,通常称为resolvereject。在执行器实现中,调用底层异步函数。如果异步函数成功,则调用resolve,如果失败,则调用reject。如果执行器函数抛出错误,则自动调用reject。您可以将任何类型的单个参数传递给resolvereject

所以我们可以这样实现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}`));
});

asyncawait使用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的不同方式。

  1. 首先,实现一些可以工作的东西,但存在我们在使用回调的讨论中看到的“回调地狱”问题的Promise版本。
  2. 接下来,将其实现为 promise chain。请注意,由于箭头函数可以使用不同的形式,因此可以使用几种不同的方法来编写它。尝试一些不同的形式。哪个最简洁?你觉得哪一本最易读?
  3. 最后,使用asyncawait来实现它。

记住,element.animate()不返回一个Promise:它返回一个带有finished属性的Animation对象,该属性是一个Promise

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1076793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TLR4-IN-C34-C2-COO,一种结合了TLR4抑制剂TLR4-IN-C34的连接器

TLR4-IN-C34-C2-COO是一种结合了TLR4抑制剂TLR4-IN-C34的连接器&#xff0c;在免疫调节中发挥重要作用&#xff0c;它通过抑制TLR4信号通路的传导&#xff0c;从而达到降低炎症反应的目的。TLR4是Toll样受体家族中的一员&#xff0c;它主要识别来自细菌和病毒的保守模式&#x…

零碳联盟:改变世界,实现绿色能源的共同梦想

如今&#xff0c;全球气候变暖已然成为我们面对的头等大事。温室气体的排放不断升高&#xff0c;导致地球温度上升&#xff0c;带来了严重的极端气候、冰川消融和海平面上升等问题。这一切都源于人类活动&#xff0c;特别是大规模使用化石燃料&#xff0c;如煤炭发电、供暖以及…

AIGC | LLM 提示工程 -- 如何向ChatGPT提问

当前生成式人工智能已经成为革命性的驱动源&#xff0c;正在迅速地重塑世界&#xff0c;将会改变我们生活方式和思考模式。LLM像一个学会了全部人类知识的通才&#xff0c;但这不意味每个人可以轻松驾驭这个通才。我们只有通过学习面向LLM的提示工程&#xff0c;才可以更好的让…

2023中考满分多少 中考总分数展示

中考总分根据地区而不同&#xff0c;以下是各地区总分数展示&#xff1a; 大部分地区的中考总分为750分&#xff0c;包括语文150分、数学150分、英语150分&#xff08;其中听力测试30分&#xff09;、思想品德与历史合卷共150分&#xff0c;物理与化学合卷共150分。 安徽中考…

计算机视觉--距离变换算法

计算机视觉 文章目录 计算机视觉前言距离变换 总结 前言 计算机视觉CV是人工智能一个非常重要的领域。 在本次的距离变换任务中&#xff0c;我们将使用D4距离度量方法来对图像进行处理。通过这次实验&#xff0c;我们可以更好地理解距离度量在计算机视觉中的应用。希望大家对计…

flutter sdk提供完整页面的ui

1.完整ui页面 可以借鉴一些使用案例&#xff1a; return Placeholder();/// A widget that draws a box that represents where other widgets will one day /// be added. /// /// This widget is useful during development to indicate that the interface is /// not yet…

总结四:数据库(MySQL)面经

文章目录 一、SQL1、介绍一下数据库分页2、介绍一下SQL中的聚合函数3、表跟表是怎么关联的?4、说一说你对外连接的了解&#xff1f;5、说一说数据库的左连接和右连接&#xff1f;6、SQL中怎么将行转成列&#xff1f;7、谈谈你对SQL注入的理解&#xff1f;8、将一张表的部分数据…

车载激光雷达标定板在无人驾驶中的作用

在自动驾驶领域&#xff0c;激光雷达的作用主要是通过扫描周围环境&#xff0c;获取车辆行驶过程中路况和障碍物的位置和形状&#xff0c;并将数据和信号传递给自动驾驶系统&#xff0c;帮助其做出相应的驾驶决策。 激光雷达使其成为自动驾驶中不可或缺的组成部分。激光雷达可以…

Dremio:新一代数据湖仓引擎

Dremio数据湖引擎 1、什么是Dremio2、什么是数据湖仓2.1、数据湖仓的历史和演变 3、Dremio查询引擎&#xff08;Dremio Sonar&#xff09;3、Dremio特点1、唯一具有自助式SQL分析功能的数据湖仓2、数据完全开放&#xff0c;无锁定3、亚秒级性能&#xff0c;云数据仓库成本的1/1…

前后端分离计算机毕设项目之基于springboot+vue的房屋租赁系统《内含源码+文档+部署教程》

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

47 从前序与中序遍历序列构造二叉树

从前序与中序遍历序列构造二叉树 先序无法确定子树大小&#xff0c;中序找不到根&#xff1b;所以用先序找根&#xff0c;用中序找大小题解1 递归题解2 迭代 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同…

世界各国家地区3d地形图

1、GPT引领前沿与应用突破之GPT4科研实践技术与AI绘图高级培训班 2、全流程R语言Meta分析核心技术 3、最新CMIP6数据处理及在气候变化、水文、生态等领域中的实践技术应用 4、WOFOST模型与PCSE模型实践技术应用 5、Biome-BGC生态系统模型与Python融合技术实践应用 6、基于…

Sqlserver查看表的主键,删除主键,修改主键

1.查看表的结构 EXEC sp_help 表名; 查询使用 sp_help 存储过程&#xff0c;它将返回有关表的详细信息&#xff0c;包括列名、数据类型、约束等。你可以在结果中查找带有 “PK” 标记的列&#xff0c;它们表示主键约束。 2.查看表的主键信息 EXEC sp_pkeys 表名; 查询使用 sp_…

直线导轨精度等级在设备中有什么影响?

直线导轨的精度选择是直线导轨应用中的重要环节&#xff0c;需要根据具体的应用场景和设备要求来选择合适的精度等级&#xff08;常见分3个等级&#xff1a;N/H/P&#xff09;。下面我们来详细了解一下直线导轨的精度选择。 1、精度等级的概念&#xff1a;直线导轨的精度等级是…

linux查看系统信息命令

1.查看linux内核版本 [rootmaster ~]# uname -r 3.10.0-123.el7.x86_64 [rootmaster ~]# uname -a Linux master 3.10.0-123.el7.x86_64 #1 SMP Mon Jun 30 12:09:22 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux2.查看文件系统的磁盘大小和剩余空间大小 [rootmaster ~]# df -h …

手机没电用日语怎么说?你会吗?柯桥常用日语学习

手机没电在日语里可以表达为: 1. スマホの電池が切れた。 直接使用“電池が切れる”来表示电池没有电了。 2. スマホのバッテリーが空に15857575376なった。 “バッテリーが空になる”也是表示电量耗尽的常用表达。 3. 充電が必要だ。 “充電が必要”意思是需要充电。 4…

使用 Splashtop 驾驭未来媒体和娱乐

在当今时代&#xff0c;数字转型不再是可选项&#xff0c;而是必选项。如今&#xff0c;媒体与娱乐业处于关键时刻&#xff0c;正在错综复杂的创意、技术和远程协作迷宫之中摸索前进。过去几年发生的全球事件影响了我们的日常生活&#xff0c;不可逆转地改变了行业的运作方式&a…

Fuzz测试 发现软件中的隐患和漏洞的秘密武器

0x01 什么是模糊测试 模糊测试&#xff08;Fuzz Testing&#xff09;是一种广泛用于软件安全和质量测试的自动化测试方法。它的基本思想是向输入参数或数据中注入随机、不规则或异常的数据&#xff0c;以检测目标程序或系统在处理不合法、不正常或边缘情况下的行为。模糊测试通…

ctDNA助力难治性RAS野生型mCRC抗EGFR单抗再挑战优势人群筛选

ctDNA检测方便快捷、安全性好、可反复取样&#xff0c;在CRC精准治疗领域具有广阔的应用前景。动态ctDNA检测有望指导患者抗EGFR单抗治疗的“再挑战”&#xff0c;为患者带来更多希望。但最佳抗EGFR单抗再挑战策略、对比标准三线治疗的疗效差异及ctDNA检测在其中扮演的角色尚无…

popcount相关性质+从低往高的数位dp:CF1734F

https://www.luogu.com.cn/problem/CF1734F popcount有个性质&#xff1a;popcount(x)^popcount(y)popcount(x^y) 考虑数位dp&#xff0c;发现很难 然后我们发现可以从低往高dp&#xff08;当做套路&#xff09; 只不过是否达到上界变成是否超出去 #include<bits/stdc.h…