JavaScript的异步编程

119 阅读2分钟

异步(Asynchronous)和同步(Synchronous)是相对的概念。

要知道,JavaScript的执行环境是 “单线程”(single thread)。

所谓 “单线程”,指的就是一次只执行一个任务。如果有多个任务,那就要排队,完成前面一个任务后,才会再执行下一个任务。

这种模式的优点是容易实现,执行环境简单;但缺点也很明显,一次只能执行一个任务,如果一个任务耗时时间长,就会拖延整个程序的执行。常见的就是服务器无响应(假死),原因就是某一段JavaScript代码执行时间长(如死循环),这就导致整个页面卡死在这里,导致其他任务无法执行。

为了解决这个问题,JavaScript将任务的执行模式分为两种:同步和异步。

在本文中,我们从同步长时间运行带来的问题开始,来了解异步编程的必要性。

一、同步编程

来看看这些代码:

const name = "Miriam";
const greeting = `Hello,my name is ${anme}!`;
console.log(greeting);// => "Hello,my name is Miriam!"

在这段代码中:

  1. 声明了一个叫做 name 的字符串变量。
  2. 声明了另一个变量 greeting 并使用了 name 的值。
  3. 输出 greeting

我们很明显能发现,代码是按照从上到下一行一行执行的。浏览器会等待代码的解析和工作,执行完上一行才会执行下一行。因为每一行新代码是建立在前面代码基础上的。

这使得它成为一个同步程序。在调用函数的时候也是同步的:

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() 就是一个同步函数,因为在函数返回之前,调用者必须等待函数完成其工作。

1.1一个耗时的同步函数

如果一个同步函数需要花费很长时间怎么办?让我们来看个例子:

<label for="quota">素数个数:</label>
<input type="text" id="quota" name="quota" value="1000000" />

<button id="generate">生成素数</button>
<button id="reload">重载</button>

<div id="output"></div>
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 =
    `完成!已生成素数${quota}个。`;
});
document.querySelector("#reload").addEventListener("click", () => {
  document.location.reload();
});

Snipaste_2025-02-28_00-04-35.png 当用户点击“生成素数”时,该程序将使用低效的算法生成素数。从点击到显示完成需要几秒钟,这取决于电脑性能。

1.2耗时同步函数的问题

在上一个例子中,我们添加一个文本框来输入内容。当点击“生成素数”后再到文本框里输入内容。

不难发现,当函数运行的时候,我们无法输入内容,点击其他按钮也没有反应。

这就是耗时的同步函数的问题,而我们需要有一种方法,让程序可以:

  • 调用一个函数来启动一个长期运行的操作。
  • 让函数开始操作并立即返回,确保程序可以对其他操作做出反应。
  • 当操作完成时返回结果。

这就是异步函数所提供的能力。

二、事件处理程序

事件处理程序(Event Handlers)是用于处理操作系统或编程语言的事件循环所发送的事件的函数或方法。通常用于响应用户的操作。

事件处理程序是异步编程的形式之一:提供的函数(事件处理程序)将在事件发生时被调用(不是立即执行)。

在早期,可以通过 XMLHttpRequest API来完成JavaScript程序向远端服务器发送 HTTP 请求。由于该操作可能需要很长的时间,所以被设计成异步API。

在这我们将创建一个新的 XMLHttpRequest 并监听它的 loadend 事件。

注意:在我们添加事件监听发送请求后,任可以在控制台输出“请求已发起”。这意味着程序可以在请求进行的同时继续运行,而事件处理程序会在请求完成时被调用。

<button id="xhr">点击发起请求</button>
<button id="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}完成!状态码:${xhr.status}`;
  });
  xhr.open(
    "GET",
    "https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json",
  );
  xhr.send();
  log.textContent = `${log.textContent}请求已发起\n`;
});
document.querySelector("#reload").addEventListener("click", () => {
  log.textContent = "";
  document.location.reload();
});

三、回调

事件处理程序是一种特殊的回调函数,回调函数曾是 JavaScript 中实现异步函数的主要方式。

回调函数在这篇文章中有详细介绍,这里不做过多讲述。juejin.cn/post/747561…

四、总结

简单来说,异步编程就是允许程序在等待某些程序完成时还可以继续执行其他任务。这种方式可以显著提高应用程序的效率和响应速度。