异步JavaScript:未来网页开发的秘密武器

发表时间: 2024-07-03 10:56

1. 概述

在这个模块,我们将查看异步 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!"

在上面的代码中,声明了一个叫 name 的字符串常量,然后声明了一个 greeting 的字符串常量。最后将 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.2. 回调

对于一个耗时的函数,势必会造成页面的卡顿,那一般的处理策略就是引入回调,当函数开始执行时立即返回,等待执行结束后通过回调将结果进行返回。

类似于点击事件的监听,回调处理是一种特殊类型的回调函数,回调函数是一个被传递到另一个函数中会在适当时机被调用的函数。但是回调函数相互嵌套会让代码难以理解。

观察下面的同步函数:

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

现在我们有一个被分成三步的操作,每一步都依赖于上一步。在这个例子中,第一步给输入的数据加 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(结果:${result3});``` ``});`` ``});`` ``});``}``doOperation();

因为必须在回调函数中调用回调函数,我们就得到了这个深度嵌套的 doOperation() 函数,这就更难阅读和调试了。在一些地方这被称为 “回调地狱” 或 “厄运金字塔”(因为缩进看起来像一个金字塔的侧面)。

面对这样的嵌套回调,处理错误也会变得非常困难:你必须在 “金字塔” 的每一级处理错误,而不是在最高一级一次完成错误处理。

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

2. Promise

Promise 是现代 JavaScript 中的异步编程基础。它是一个由异步函数返回的对象,可以指示操作当前所处的状态。在 Promise 给调用者的时候,操作往往没有完成,但 Promise 对象提供了方法来处理最终的成功或失败。

2.1. Promise 基本介绍

首先,Promise 有三种状态:

  • 待定(pending) :初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch() 返回 Promise 时的状态,此时请求还在进行中。
  • 已兑现(fulfilled) :意味着操作成功完成。当 Promise 完成时,它的 then() 处理函数被调用。
  • 已拒绝(rejected) :意味着操作失败。当一个 Promise 失败时,它的 catch() 处理函数被调用。

有时我们用已敲定(settled)这个词来同时表示已兑现(fulfilled)和已拒绝(rejected)两种情况。

如果一个 Promise 已敲定,或者如果它被 “锁定” 以跟随另一个 Promise 的状态,那么它就是已解决(resolved)的。

比如定义一个网络请求:

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(已收到响应:${response.status});```}); console.log(``"已发送请求……"``);

在这个实例中,执行过程如下:

  1. 调用 fetch () API,并将返回值赋给 fetchPromise 变量;
  2. 输出 fetchPromise 变量,输出结果应该像这样:Promise { <state>: "pending" }。这告诉我们这个 Promise 对象,有一个 state 属性,值为 ”pending“。”pending“ 状态意味着操作仍在进行中;
  3. 将一个处理函数传递给 Promise 的 then () 方法。当方法操作成功时,Promise 将调用我们的处理函数,传入一个包含服务器影响的 Response 对象;
  4. 输出一条信息,说明我们已经发送了这个请求。

Promise 的优雅之处在于 then () 本身也会返回一个 Promise,这个 Promise 将指示 then () 中调用的异步函数的完成状态。所以对于嵌套场景时就可以改成链式。

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 链,意味着当我们需要连续进行异步函数调用时,我们就可以避免不断嵌套带来的缩进增加。

2.2. 合并多个 Promise

当多个异步操作时,出现多个 Promise 返回值,此时,需要我们对 Promise 进行合并。这里就需要使用 Promise.all () 方法。

const fetchPromise1 = ``fetch``(`` ``"
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"``,``);``const fetchPromise2 = ``fetch``(`` ``"
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found"``,``);``const fetchPromise3 = ``fetch``(`` ``"
https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json"``,``); Promise.``all``([fetchPromise1, fetchPromise2, fetchPromise3])`` ``.``then``((responses) => {`` ``for (const response ``of responses) {`` ```console.log(response.url:{response.url}:response.url:{response.status});``` ``}`` ``})`` ``.catch((error) => {`` ```console.error(获取失败:${error});``` ``});

由 Promise.all() 返回的 Promise:

  • 当且仅当数组中所有的 Promise 都被兑现时,才会通知 then() 处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入 all() 的 Promise 的顺序相同。
  • 会被拒绝 —— 如果数组中有任何一个 Promise 被拒绝。此时,catch() 处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。

3. async 和 await

3.1. async

async 关键字为你提供了一种更简单的方法来处理基于异步 Promise 的代码。在一个函数的开头添加 async,就可以使其成为一个异步函数。在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

async ``function myFunction() {`` ``// 这是一个异步函数``}

比如:

async ``function f() {`` ``return 1;``} f().``then``(alert); // 1

所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。

3.2. await

语法如下:

// 只在 async 函数内工作``let value = await promise;

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

在异步函数中,你可以在调用一个返回 Promise 的函数之前使用 await 关键字。这使得代码在执行时进行等待,直到 Promise 被完成,这时 Promise 的影响被当做返回值,或者被拒绝的响应作为错误抛出。

async ``function fetchProducts() {`` ``try {`` ``` // 在这一行之后,我们的函数将等待 fetch()调用完成``` ``` // 调用 fetch() 将返回一个“响应”或抛出一个错误``` ``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 请求错误:
{response.status}`);``` ``}`` ```// 在这一行之后,我们的函数将等待 `response.json()` 的调用完成``` ```// `response.json()` 调用将返回 JSON 对象或抛出一个错误``` ``const json = await response.json();`` ``console.log(json[0].``name``);`` ``} catch (error) {`` ```console.error(`无法获取产品列表:{error});``` ``}``} fetchProducts();

这里我们调用 await fetch(),我们的调用者得到的并不是 Promise,而是一个完整的 Response 对象,就好像 fetch() 是一个同步函数一样。请注意你只能在 async 函数中使用 await。

3.3. 自定义 Promise

Promise() 构造器使用单个函数作为参数。我们把这个函数称作执行器(executor)。当你创建一个新的 promise 的时候你需要实现这个执行器。

这个执行器本身采用两个参数,这两个参数都是函数,通常被称作 resolve 和 reject。在你的执行器实现里,你调用原始的异步函数。如果异步函数成功了,就调用 resolve,如果失败了,就调用 reject。如果执行器函数抛出了一个错误,reject 会被自动调用。你可以将任何类型的单个参数传递到 resolve 和 reject 中。

function alarm(person, delay) {`` ``return new Promise((resolve, reject) => {`` ``if (delay < 0) {`` ``throw new Error(``"Alarm delay must not be negative"``);`` ``}`` ``window.setTimeout(() => {`` ```resolve(Wake up, ${person}!);``` ``}, delay);`` ``});``}

此函数创建并且返回一个新的 Promise。对于执行器中的 promise,我们:

  • 检查 delay(延迟)是否为负数,如果是的话就抛出一个错误。
  • 调用 window.setTimeout(),传递一个回调函数和 delay(延迟)。当计时器过期时回调会被调用,在回调函数内,我们调用了 resolve,并且传递了 "Wake up!" 消息。

4. 总结

Promise 是现代 JavaScript 异步编程的基础。它避免了深度嵌套回调,使表达和理解异步操作序列变得更加容易,并且它们还支持一种类似于同步编程中 try...catch 语句的错误处理方式。

async 和 await 关键字使得从一系列连续的异步函数调用中建立一个操作变得更加容易,避免了创建显式 Promise 链,并允许你像编写同步代码那样编写异步代码。