JavaScript 的异步性 - Promises、回调和 async/await

2025-06-08

JavaScript 的异步性 - Promises、回调和 async/await

JavaScript 的核心概念之一是异步性,这意味着可以同时执行多项操作。它是一种避免代码被耗时操作(例如 HTTP 请求)阻塞的解决方案。在本文中,您将学习异步性的基本概念以及如何在 JavaScript 中使用它。


但在我们开始之前...

……我们需要了解一些计算机理论。编程是告诉计算机它应该做什么的过程,我们用代码与计算机沟通。每段代码都只是一组我们想要执行的机器指令。我们的每一行代码都由所谓的线程执行。一个线程一次只执行一条指令。让我们分析一下这段伪代码:

set x to 10
set y to 5
add x to y save result to r
display r
Enter fullscreen mode Exit fullscreen mode

当我们执行此代码时,线程将首先将变量x值设置为 10,然后将其设置y为 5,之后将这两个数字相加并将结果保存到变量中r,最后显示 r 的值。关键字是THENAFTER THAT,我们的线程不能同时将其设置x为 10 和y5,它必须等待设置y完成才能x进行设置。这种类型的代码称为同步代码 - 每条指令都是依次执行的。通过如此简单的操作,我们不会看到任何问题,但是当我们想要执行一些耗时的操作时该怎么办?例如下载图像?嗯,这里有一个棘手的部分。

这样的操作是阻塞代码,因为它会阻止我们的线程执行任何其他操作,直到图片下载完成。我们不希望用户每次遇到这样的指令时都等待。想象一下,您正在下载一个表情包,而下载过程中您的电脑无法执行任何其他操作——音乐播放器停止播放、桌面冻结等等——使用这样的电脑会非常痛苦。您可能已经注意到,这些情况不会发生,您可以同时听音乐、在 YouTube 上观看视频,并编写您的突破性项目。这是因为计算机工程师找到了解决这个问题的方法。

聪明人曾经想过,如果一个线程可以一次执行一个操作,那么16个线程难道不能并行执行16个操作吗?是的,可以——这就是为什么现代CPU拥有多核,并且每个核都拥有多个线程的原因。使用多线程的程序就是多线程的

显示多线程应用程序如何工作的图表。

JavaScript 的问题在于它不是多线程的,而是单线程的,所以它无法使用多个线程同时执行多个操作。我们又遇到了同样的问题——还有其他方法可以解决这个问题吗?有的!那就是编写异步代码

假设您希望在用户每次滚动您的网站时从服务器获取帖子。为此,我们需要进行 API 调用。API 调用只是 HTTP 请求,这意味着我们的浏览器进行此类调用需要与我们的服务器建立连接,然后我们的服务器处理该请求,然后将其发送回,然后我们的浏览器需要处理它......这一切都很耗​​时,等待它完成会阻止我们网站上的其他交互,但这只有在我们的代码是同步的情况下才会发生。大多数耗时的事情(例如 HTTP 请求)大多不是由我们的主线程处理的,而是由我们浏览器中实现的低级 API 处理的。异步代码使用这个原则。我们不必等待浏览器完成 HTTP 请求,我们只需通知浏览器我们需要发出 HTTP 请求,浏览器将处理它并向我们报告结果 - 同时,其他代码可以在主线程上执行。

异步代码工作原理图

你可能注意到了,异步代码和多线程代码很相似。嗯,某种程度上是这样。两者都能帮助我们解决阻塞代码的问题,但JavaScript 中的异步代码是伪并行的。例如,如果我们想并行运行两个计算密集型的计算,就必须等到其他程序(比如浏览器的底层 API)处理完之后才能执行。为了在 JavaScript 中实现真正的并行,我们可以使用WebWorkers,它在后台运行指定的代码。不过,WebWorkers不是今天的主题,所以我暂时不讨论它们。😉

好了,理论讲得够多了。那么,我们该如何在 JavaScript 中编写异步代码呢?主要有两种方法:一种是使用回调函数的旧方法,另一种是使用Promises的新方法。现在,我们来深入研究一下它们。

回调

之前我说过,异步操作完成后,我们会通知主线程。之前常用的反馈方式是使用回调函数回调函数本质上是一个在任务完成时调用的函数。它也可以携带参数,例如异步任务的结果。我们来分析一些例子。

我们将使用API 从pokeapi.coXMLHttpRequest获取有关 Charmander 的信息。

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://pokeapi.co/api/v2/pokemon/charmander', true);
xhr.responseType = 'json';
xhr.onload = (e) => {
  if (xhr.status === 200) {
    console.dir(xhr.response);
  } else {
    console.error('Something went wrong...');
  }
};
xhr.send(null);
Enter fullscreen mode Exit fullscreen mode

前三行只是配置XMLHttpRequest对象。我们最感兴趣的是xml.onload,因为这里我们使用箭头函数指定了回调函数。当我们发送请求时,浏览器会处理它,处理完成后,它会调用我们的回调函数,我们可以在其中进一步处理接收到的数据。

使用回调处理异步任务的另一个常见示例是事件监听器。请看下面的代码。

const button = document.getElementById('myButton');
button.addEventListener('click', (event) => {
  console.info('Button clicked!');
});
Enter fullscreen mode Exit fullscreen mode

我们使用按钮元素的 ID 获取其事件,然后为其附加一个监听器click。监听器函数其实就是回调函数。每次用户点击按钮时,箭头函数都会被调用。整个过程不会阻塞代码,因为我们不必在主线程中等待点击事件。事件由浏览器处理,我们只在点击完成后附加一个回调函数。

再举一个例子。超时间隔也是异步的。

const timeout = setTimeout(() => {
  console.info('Boo!');
}, 5000);
Enter fullscreen mode Exit fullscreen mode

TimeoutInterval处理函数也是一个回调函数,它仅在经过一定时间后才会被调用。整个时间测量代码由浏览器组件处理,而不是我们自己,因此只有在经过了正确时间后才会通知我们。

现在让我们结合其中一些例子进行回顾。

const button = document.getElementById('myButton');
button.addEventListener('click', (event) => {
  console.info('Button clicked!');
});

const request = setTimeout(() => { // This timeout is going to simulate a very long HTTP request
  console.info('Response received!');
}, 5000);
Enter fullscreen mode Exit fullscreen mode

在这段代码中,我们给按钮附加了一个监听器,并发出了 HTTP 请求。运行此示例,您会发现尽管 HTTP 请求正在发出,但您仍然可以点击按钮。您无需等待按钮被点击后再发送请求,也无需等待 HTTP 请求完成后再处理按钮点击——没有任何操作会被阻塞。这就是异步的强大之处!

承诺

在 JavaScript 中处理异步性的现代方法是使用Promises。你可以把它们想象成人们做出的承诺。它不是某件事的结果,而只是对将来会做(或不做)某事的承诺。如果你的室友答应你这周倒垃圾,她是在告诉你她将来会做,但不是现在。你可以专注于你的事情,几个小时后你的室友会告诉你垃圾桶是空的,她履行了她的诺言。你的室友也可以告诉你,她无法履行诺言,因为你的垃圾桶里住着一只浣熊,当你试图拿出垃圾袋时,它表现得很有攻击性。在这种情况下,她无法遵守这个诺言,因为她不想被一只好斗的浣熊袭击。

浣熊的照片
记住,并非所有浣熊都具有攻击性!图片由Vincent Dörig在 Unsplash 上拍摄

Promise可以处于以下三种状态之一:

  • 待定- 这是初始状态,Promise 正在运行,我们不知道它是否已实现或是否出现了问题。
  • 已实现(或已解决)- 一切正常。Promise 已成功完成其任务。
  • 拒绝- 出现问题,操作失败。

因此让我们创建我们的第一个承诺

const promise = new Promise((resolve) => {
  setTimeout(resolve, 3000);
});
Enter fullscreen mode Exit fullscreen mode

我们通过调用Promise构造函数创建一个新的Promise对象。如您所见,在此示例中,Promise对象的构造函数接受一个箭头函数作为参数。这个参数称为executorexecutor function。当我们创建Promise对象时,将调用executor ,它是Promise和结果之间的连接器。executor 接受两个参数:一个resolve 函数和一个rejection 函数- 它们都用于控制您的 Promise。Resolve 用于将我们的承诺标记为已实现并返回结果数据。Reject 用于通知出现问题并且 Promise 将无法实现 - 它被拒绝了。像 resolve 一样,Reject 也可以携带数据,在大多数情况下,它携带有关为何Promise未实现的信息。

Promise对象提供的方法可以处理 Promise 的解决和拒绝。请查看以下代码。

const promise = new Promise((resolve) => {
  setTimeout(resolve, 3000);
});

promise.then(() => {
  console.info('3 seconds have passed!');
});
Enter fullscreen mode Exit fullscreen mode

我们的 Promise 非常简单,执行器会创建一个 Timeout 并在 3 秒后调用我们的 resolve 函数。我们可以.then()通过提供一个回调函数来拦截此信息。.then()它接受两个参数,第一个是 Promise 完成时调用的回调函数,第二个(本例中未显示)是 Promise 被拒绝时调用的回调函数。但是,对于处理被拒绝的 Promise,我们可以使用一种更方便的方法—— .catch()。让我们修改一下示例。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const number = Math.floor(Math.random()*100);

    if (number % 2 === 0) {
      resolve(number);
    }

    reject(new Error('Generated number is not even!'));
  }, 3000);
});

promise.then((result) => {
  console.info('Promise fulfilled!');
  console.info(`${result} is even.`);
}).catch((error) => {
  console.info('Promise rejected!');
  console.error(error);
});
Enter fullscreen mode Exit fullscreen mode

这段代码会在 3 秒后生成一个随机数,并检查它是否为偶数。如果是偶数,则 Promise 被解决,并返回该偶数;如果不是,则拒绝 Promise 并显示错误消息。.catch()函数接受一个回调函数作为参数,该回调函数在 Promise 被拒绝时调用。

我们还可以通过抛出错误来拒绝 Promises。

const promise = new Promise((resolve) => {
  throw new Error('Error message');
});

promise.then((result) => {
  console.info('Promise fulfilled!');
}).catch((error) => {
  console.info('Promise rejected!');
  console.error(error);
});
Enter fullscreen mode Exit fullscreen mode

然而,这种方法也有一些局限性。如果我们在异步函数(比如示例中的 Timeout 回调)中抛出错误,错误.catch()将不会被调用,抛出的错误将表现为未捕获的错误

const promise = new Promise((resolve) => {
  setTimeout(() => {
    const number = Math.floor(Math.random()*100);

    if (number % 2 === 0) {
      resolve(number);
    }

    throw new Error('Generated number is not even!'); // This is an Uncaught Error
  }, 3000);
});

promise.then((result) => {
  console.info('Promise fulfilled!');
  console.info(`${result} is even.`);
}).catch((error) => {
  console.info('Promise rejected!');
  console.error(error);
});
Enter fullscreen mode Exit fullscreen mode

此外,您需要记住,调用后引发的每个错误resolve()都将被忽略。

const promise = new Promise((resolve) => {
  resolve();
  throw new Error('Error message'); // This is silenced
});
Enter fullscreen mode Exit fullscreen mode

除了.then()和 之外,.catch()我们还有第三种方法 - .finally()。Finally 在 Promise 完成时被调用,它并不关心它是被解决还是被拒绝,它在.then()和 之后运行.catch()

const promise = new Promise((resolve, reject) => {
  if (Math.random() < 0.5) {
    resolve('Promise fulfilled');
  }

  reject(new Error('Promise rejected'));
});

promise.then((result) => {
  console.dir(result); // Runs only when the Promise is resolved
}).catch((error) => {
  console.error(error); // Run only when the Promise is rejected
}).finally(() => {
  console.dir('Promise has finished its work'); // Run everytime the Promise is finished
});
Enter fullscreen mode Exit fullscreen mode

现在,我们来分析一个真实案例。

const fetchCharmanderData = fetch('https://pokeapi.co/api/v2/pokemon/charmander');

fetchCharmanderData.then((response) => {
  if (response.status === 200) {
    return response.json();
  } else {
    throw new Error(response.statusText);
  }
}).then((data) => {
  console.dir(data);
}).catch((error) => {
  console.error(error);
});
Enter fullscreen mode Exit fullscreen mode

此代码将从pokeapi.co获取有关 Charmander 的信息,但它使用了新的基于承诺的fetch API。Fetch将发出 HTTP 请求并返回一个 Promise。获取数据后,我们将处理响应。如果我们收到 HTTP 状态 200(OK),我们将返回响应主体的 JSON 表示形式,如果状态代码不同(如 404 未找到或 500 内部服务器错误),我们将抛出一个带有状态消息的错误。如您所见,我们使用了.then()两次。正如我所提到的,第一次用于处理响应,第二次我们用来.then()处理第二个 Promise。response.json()也返回一个 Promise(JSON 解析也需要一些时间,所以它也可能阻塞代码,这就是我们要使其异步的原因)。基本上,这向我们证明你可以有一个 Promise 来解析另一个 Promise,并且你可以通过链接控制方法(如)来一个接一个地then处理catch它们finally

异步/等待

链式调用.then()有时.catch().finally()很麻烦,导致代码难以阅读。ES8(或 EcmaScript 2017)引入了一些语法糖,以便更轻松地处理 Promise—— asyncawait。让我们使用 async/await 重写我们的 Charmander 示例。

(async () => {
  const response = await fetch('https://pokeapi.co/api/v2/pokemon/charmander');

  try {
    if (response.status === 200) {
      const charmanderData = await response.json();
      console.dir(charmanderData);
    } else {
      throw new Error(response.statusText);
    }
  } catch (error) {
    console.error(error);
  }
})();
Enter fullscreen mode Exit fullscreen mode

这段代码的功能与之前的代码完全相同 - 只是编写方式不同。我们不能在异步函数之外使用awaitfetch() ,因此我们通过创建一个自调用异步函数来绕过它。在这个函数内部,我们正在等待返回的响应。收到响应后,我们将检查其状态代码,如果没问题,我们将等待响应主体被解析,然后将其输出。您可能注意到缺少了.catch()。我们用 try-catch 块替换了它,基本上,它将执行与 相同的操作.catch()。如果其中的任何东西try引发错误,代码将停止执​​行,并catch改为运行其中的错误处理代码。

我提到了异步函数,并且 await 只能在异步函数内部使用。它是 ES8 中引入的一种新函数类型,简而言之,它是一个利用基于 Promise 行为的函数,这意味着异步函数总是返回一个 Promise。它可以在另一个异步函数中被 await,或者像 Promise 一样被处理。

async function getCharmanderData() {
  const response = await fetch('https://pokeapi.co/api/v2/pokemon/charmander');
  return response.json();
}

(async () => {
  console.dir(await getCharmanderData());
})();
Enter fullscreen mode Exit fullscreen mode

注意:此示例已简化,现在不包含任何异常处理。

我们将负责从pokeapi.co获取小火龙数据的逻辑移到了异步函数中。这样,每次我们需要这些数据时,只需使用 await 调用这个函数即可,无需编写冗长的 Promise 链。

我说过异步函数可以被视为 Promise,下面是我们如何做到这一点的一个例子。

async function getCharmanderData() {
  const response = await fetch('https://pokeapi.co/api/v2/pokemon/charmander');
  return response.json();
}

getCharmanderData().then((data) => {
  console.dir(data);
});
Enter fullscreen mode Exit fullscreen mode

Await 也可以用于返回 Promise 的普通函数。

function delay(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

(async () => {
  console.info('Start!');
  await delay(5000);
  console.info('5 seconds have passed.');
})();
Enter fullscreen mode Exit fullscreen mode

承诺助手

Promise对象还有一些非常有用的方法,可以帮助我们处理许多 Promise。

Promise.all()

Promise.all()等待所有传递的 Promise 得到实现并将所有结果解析为一个数组。

const charmander = fetch('https://pokeapi.co/api/v2/pokemon/charmander').then((response) => response.json());
const bulbasaur = fetch('https://pokeapi.co/api/v2/pokemon/bulbasaur').then((response) => response.json());
const squirtle = fetch('https://pokeapi.co/api/v2/pokemon/squirtle').then((response) => response.json());

Promise.all([charmander, bulbasaur, squirtle]).then((result) => {
  console.dir(result);
});
Enter fullscreen mode Exit fullscreen mode

值得一提的是,当传递的一个承诺被拒绝时,Promise.all()承诺也会被拒绝。

Promise.allSettled()

它类似于,但当传递的一个(或多个)承诺被拒绝Promise.all()时,它不会被拒绝

const charmander = fetch('https://pokeapi.co/api/v2/pokemon/charmander').then((response) => response.json());
const fail = fetch('https://pokeapi.co/api/v2/pokemon/non-existing').then((response) => response.json()); // This Promise is going to fail
const squirtle = fetch('https://pokeapi.co/api/v2/pokemon/squirtle').then((response) => response.json());

Promise.allSettled([charmander, fail, squirtle]).then((result) => {
  console.dir(result);
});
Enter fullscreen mode Exit fullscreen mode

Promise.any()

Promise.any()当传递的任何一个 Promise 都已实现时,它就会被实现。它还会返回第一个已解决的Promise的结果。当传递的 Promise 均未实现时,Promise.any()它将被拒绝。

const charmander = fetch('https://pokeapi.co/api/v2/pokemon/charmander').then((response) => response.json());
const bulbasaur = fetch('https://pokeapi.co/api/v2/pokemon/bulbasaur').then((response) => response.json());
const squirtle = fetch('https://pokeapi.co/api/v2/pokemon/squirtle').then((response) => response.json());

Promise.any([charmander, bulbasaur, squirtle]).then((result) => {
  console.dir(result);
});
Enter fullscreen mode Exit fullscreen mode

Promise.race()

当任何传递的承诺被解决或拒绝时,它就会得到解决。

const charmander = fetch('https://pokeapi.co/api/v2/pokemon/charmander').then((response) => response.json());
const bulbasaur = fetch('https://pokeapi.co/api/v2/pokemon/bulbasaur').then((response) => response.json());
const squirtle = fetch('https://pokeapi.co/api/v2/pokemon/squirtle').then((response) => response.json());

Promise.race([bulbasaur, charmander, squirtle]).then((result) => {
  console.dir(result);
});
Enter fullscreen mode Exit fullscreen mode

现在你应该对 JavaScript 的异步性有了更好的理解。作为家庭作业,尝试使用pokeapi.coFetch API。创建自定义 Promise,使其在一定延迟后获取 Pokemon,或者根据之前 Promise 中收到的内容获取数据。你还可以在代码中使用 async/await 和 Promise 助手来进一步探索这个主题。再见(或者读到你的文章?),祝你编程愉快!

PS:如果您喜欢我的作品,请记得查看我的博客并考虑订阅我的时事通讯(只有好内容,没有垃圾邮件,我保证😃)

鏂囩珷鏉ユ簮锛�https://dev.to/danieo/javascript-s-asynchronicity-promises-callbacks-and-async-await-31l9
PREV
你知道是什么激励你成为一名程序员吗?基于需求优先级的激励 其他激励来源 你的动力
NEXT
水管工云指南