异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。
异步 JavaScript 简介
异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果。
同步编程
同步程序是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上的。
**同步函数:**因为在函数返回之前,调用者必须等待函数完成其工作。
存在问题:如果一个同步函数消耗时间很长,只能等待不能做其他的事情。
解决方法:
- 通过调用一个函数来启动一个长期运行的操作
- 让函数开始操作并立即返回,这样我们的程序就可以保持对其他事件做出反应的能力
- 当操作最终完成时,通知我们操作的结果。
事件处理程序
事件处理程序实际上就是异步编程的一种形式:
- 提供的函数(事件处理程序)将在事件发生时被调用(而不是立即被调用)。
- 如果“事件”是“异步操作已经完成”,事件处理程序被用来通知调用者异步函数调用的结果,这就是一种异步的方式。
回调
回调函数是一个被传递到另一个函数中的会在适当的时候被调用的函数。
事件处理程序就是一种特殊类型的回调函数。
当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解。(回调函数中调用回调函数)
当你需要执行一些分解成一系列异步函数的操作时,这将变得十分常见。例如下面这种情况:
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();
由于以上这些原因,大多数现代异步 API 都不使用回调。
事实上,JavaScript 中异步编程的基础是 Promise
。
参考资料:
JavaScript 异步编程 | 菜鸟教程
异步 JavaScript 简介 - 学习 Web 开发 | MDN
使用 Promise
Promise 是现代 JavaScript 中异步编程的基础。它是一个由异步函数返回的对象,可以指示操作当前所处的状态。
在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象提供了方法来处理操作最终的成功或失败。
使用fetch() API
全局 fetch() 方法用于发起获取资源的请求,它会返回一个会在请求响应后兑现的 promise。
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("已发送请求……");
than方法:将一个处理函数传递给 Promise 的 then() 方法。当(如果)获取操作成功时,Promise 将调用我们的处理函数,传入一个包含服务器的响应的 Response 对象。
完整的输出结果应该是这样的:
Promise { <state>: "pending" }
已发送请求……
已收到响应:200
已发送请求…… 的消息在我们收到响应之前就被输出了。与同步函数不同,fetch() 在请求仍在进行时返回,这使我们的程序能够保持响应性。响应显示了 200(OK)的状态码,意味着我们的请求成功了。
链式使用 Promise
我们想获得 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((json) => {
console.log(json[0].name);
});
});
前面说过,在回调中调用另一个回调会出现多层嵌套的情况,这种“回调地狱”使我们的代码难以理解,这不是也一样吗,只不过变成了用 then() 调用而已?
但 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);
});
直接返回 json() 返回的 Promise,并在该返回值上调用第二个 then()。这被称为 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 请求错误:${response.status}`);
}
return response.json();
})
.then((json) => {
console.log(json[0].name);
});
错误捕获
fetch() API 可能因为很多原因抛出错误(例如,没有网络连接或 URL 本身存在问题),我们也会在服务器返回错误消息时抛出一个错误。
为了支持错误处理,Promise 对象提供了一个 catch() 方法。
- 当异步操作_成功_时,传递给 then() 的处理函数被调用,
- 当异步操作_失败_时,传递给 catch() 的处理函数被调用。
如果将 catch() 添加到 Promise 链的末尾,它就可以在任何异步函数失败时被调用。
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 请求错误:${response.status}`);
}
return response.json();
})
.then((json) => {
console.log(json[0].name);
})
.catch((error) => {
console.error(`无法获取产品列表:${error}`);
});
合并使用多个 Promise
你可能需要合并多个异步函数的调用,Promise API 为解决这一问题提供了帮助。
方式一:需要所有的 Promise 都得到实现,但它们并不相互依赖。在这种情况下,将它们一起启动然后在它们全部被兑现后得到通知会更有效率。
这里需要 Promise.all() 方法。它接收一个 Promise 数组,并返回一个单一的 Promise。
由Promise.all()返回的 Promise:
- 当且仅当数组中_所有_的 Promise 都被兑现时,才会通知 then() 处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入 all() 的 Promise 的顺序相同。
- 会被拒绝——如果数组中有_任何一个_ Promise 被拒绝。此时,catch() 处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。
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(
"bad-scheme://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.status}`);
}
})
.catch((error) => {
console.error(`获取失败:${error}`);
});
方式二:需要一组 Promise 中的某一个 Promise 的兑现,而不关心是哪一个。
在这种情况下,你需要 Promise.any()。这就像 Promise.all(),不过在 Promise 数组中的任何一个被兑现时它就会被兑现,如果所有的 Promise 都被拒绝,它也会被拒绝。
在这种情况下,无法预测哪个获取请求会先被兑现。
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.any([fetchPromise1, fetchPromise2, fetchPromise3])
.then((response) => {
console.log(`${response.url}:${response.status}`);
})
.catch((error) => {
console.error(`获取失败:${error}`);
});
Promise相关概念
首先,Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch() 返回 Promise 时的状态,此时请求还在进行中。
- 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的 then() 处理函数被调用。
- 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的 catch() 处理函数被调用。
有时我们用已敲定(settled)这个词来同时表示已兑现(fulfilled)和已拒绝(rejected)两种情况。
:::tips
注意:
这里的“成功”或“失败”的含义取决于所使用的 API:例如,fetch() 认为服务器返回一个错误(如 404 Not Found)时请求成功,但如果网络错误阻止请求被发送,则认为请求失败。
:::
async 和 await
async 关键字为你提供了一种更简单的方法来处理基于异步 Promise 的代码。
- 在一个函数的开头添加 async,就可以使其成为一个异步函数。
- 在异步函数中,在调用一个返回 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}`);
}
}
const promise = fetchProducts();
//方式1 正确
promise.then((data) => console.log(data[0].name));
//方式2 错误
console.log(promise[0].name); // “promise”是一个 Promise 对象,因此这句代码无法正常工作
//方式3 正确
console.log(promise[0].name); // “promise”是一个 Promise 对象,因此这句代码无法正常工作
:::tips
注意:
请注意你只能在 async 函数中使用 await,不能在顶层或者同步函数使用,除非代码是 JavaScript 模块。
:::
参考资料:
如何使用 Promise - 学习 Web 开发 | MDN