JavaScript 中的异步

promise 可以解决异步操作中的回调地狱难题,在详细了解 promise 之前,先看看 JavaScript 中的异步是怎么实现的。

什么是异步编程

异步编程可以使得程序在执行一个可能长期运行的任务的同时,可以对其他事件做出反应,不需要等待长期任务完成,并且长期任务完成后也可以显示结果。

同步编程

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

这段代码:

  1. 声明了一个字符串常量 name
  2. 声明了另一个字符串常量 greeting ,并在其中用到了 name 的值
  3. 将 greeting 输出到 JavaScript 控制台中。

浏览器实际上就是按照 JavaScript 中代码的顺序一行一行地执行程序的,在每一行等待代码的解析和工作,完成上一行才执行下一行。每一行新的代码都是建立在前面的代码的基础之上的,所以这样逐行执行是很有必要的。

这,使得这段代码称为一个 同步程序

调用函数的时候也是同步的:

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

当函数耗时较大时

这段代码在点击“生成素数”按钮时,会用一种非常低效的算法生成素数,也可以控制要生成的素数的数量,这对操作需要的时间也有影响。

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

点击“生成素数”按钮后,需要几秒钟才会显示“完成!”信息。

也就是说,在 generatePrimes() 函数运行时,程序没有其他的反应。

这就是耗时较长的同步函数的基本问题。希望有一种方法可以让程序:

  • 通过调用一个函数来启动一个长期运行的操作
  • 让函数开始操作并立即返回,这样我们的程序就可以保持对其他事件做出反应的能力
  • 当操作完成时,通知我们操作的结果

这就是异步函数的能力。

事件处理程序

以上对异步函数能力的描述容易令人想起事件处理程序

事实上事件处理程序就是异步编程的一种形式:我们提供的函数(事件处理程序)将在事件发生时被调用(而不是立即调用)(如同点击事件)。如果“事件”是“异步操作已经完成”,那么我们就可以看到事件是如何被用来通知调用者,通知的内容是异步函数调用的结果。

一些早期的 API 就是用这种方式来使用事件的。 XMLHttpRequest API 可以让我们使用 JavaScript 向远程服务器发起 HTTP 请求。由于这样的操作可能需要很长的时间,所以它被设计成异步 API,我们可以通过给 XMLHttpRequest 对象附加事件监听器来让程序在请求进展和最终完成时获得通知。

下面这段代码点击 “点击发起请求” 按钮来发送一个请求。创建一个新的 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();
});

注意:

控制台上输出顺序是:

请求已发起
完成!状态码:200

类似于模块中遇到的事件处理程序,区别是,事件不是点击按钮那样的用户行为,而是某个对象的状态变化。

回调

事件处理程序是一种特殊类型的回调函数。回调函数是一个被传递到另一个函数中的,会在适当时候被调用的函数。

上面展示过,回调函数曾经是 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}`);
}
doOperation();

这里的每一步都依赖于上一步,很容易理解。

回调函数来实现。

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(`结果:${result3}`);
      });
    });
  });
}
doOperation();

因为必须在回调函数中调用回调函数,我们得到了这个深度嵌套的 doOperation() 函数,难以阅读和调试,被称为 “回调地狱” 或者 “厄运金字塔” 。

处理错误只能在每一层的回调中处理,不能在最高级中依次完成错误处理。

基于以上这些原因,大多数现代异步API都不使用回调。而 JavaScript 中异步编程的基础,就是 Promise 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值