在我们深入探讨异步 JavaScript 之前,先来理解一下什么是同步的 JavaScript,以及为什么我们需要异步的方式来编写 JavaScript 代码,对吧?
什么是同步的 JavaScript?
在同步编程模式下,任务是一个接一个地按顺序执行的。下一个任务必须等到当前任务完成后才能开始。如果某项任务耗时较长,那么其他所有任务都得等着。
想象一下你在超市排队结账的情形:如果你前面的人买了很多东西,花了很长时间才结完账,那你只能等他们搞定了才能轮到你。
console.log("开始烹饪");
for (let i = 0; i < 5; i++) {
console.log("烹饪菜品 " + (i + 1));
}
console.log("完成烹饪");
发生的情况如下:
-
打印
"开始烹饪"。 -
然后进入一个循环,依次打印每个
"烹饪菜品 X"。 -
循环结束后,打印
"完成烹饪"。
在这个例子中,代码是按顺序运行的,当前的任务(每道菜的烹饪)没有完成之前,其他的事情都无法进行。想象一下如果有一道菜需要煮十分钟,那么其他所有的事情都得等这道菜煮好才行。
什么是异步的 JavaScript?
在异步编程模式下,你可以启动一个任务,同时这个任务还在运行(比如等待来自服务器的数据),其他任务可以继续执行。你不必等一个任务完成了才能开始另一个。
想象一下你在餐厅点餐的情景:你点好餐之后,可以继续跟朋友聊天或者玩手机,等到食物准备好了,服务员会把它们端上来。
异步代码示例:
console.log("开始烹饪");
setTimeout(() => {
console.log("烹饪完成!");
}, 3000); // 模拟一个耗时三秒的任务
console.log("等待的同时可以做其他事情");
发生的情况如下:
-
打印
"开始烹饪"。 -
setTimeout函数启动了一个三秒的计时器。但是它不会等待,JavaScript 会立刻执行下一行代码。 -
打印
"等待的同时可以做其他事情"。 -
三秒后,计时器结束并打印
"烹饪完成!"。
编写异步 JavaScript 主要有三种方法:
-
回调函数
-
Promise
-
Async/Await
这些都是处理 JavaScript 中异步代码的主要方法。
回调函数
JavaScript 中的回调函数是你作为参数传递给另一个函数的一个函数。基本的思想是你将一个函数作为参数传入另一个函数,而这个传入的函数被称为“回调函数”。当某个任务完成之后,通常是在异步操作(如从服务器获取数据)之后,就会调用这个回调函数。
这使得你的主函数可以在不等待任务完成的情况下继续做其他事情。当任务完成时,回调会被触发来处理结果。
function mainFunc(callback){
console.log("这是在 setTimeout 之前设置的")
callback()
console.log("这是在 setTimeout 之后设置的")
}
function cb(){
setTimeout(()=>{
console.log("这应该是在三秒后显示的内容")
},3000)
}
mainFunc(cb)
这段代码展示了 JavaScript 中的 回调 概念。具体过程如下:
-
mainFunc接受一个 回调函数 作为参数。 -
在
mainFunc内部,回调函数 立即被执行,但由于该 回调函数 (cb函数)包含了setTimeout,它安排了console.log在三秒后执行。 -
同时,
mainFunc继续执行,在调用回调之前和之后打印信息。 -
三秒后,回调内部的
setTimeout完成,延迟的信息被打印出来。
这展示出主函数如何在不等待异步操作(回调中的三秒延迟)完成的情况下继续执行它的任务。
什么是回调地狱以及何时会发生?
回调地狱 是指 JavaScript 中多个回调函数以不可管理的方式相互嵌套,导致代码难以阅读、维护和调试。通常看起来像是一堆嵌套的函数金字塔,其中每个异步操作依赖于前一个操作的完成,从而导致层层嵌套的回调。
回调地狱通常发生在你需要按顺序执行多个异步任务,且每个任务依赖于前一个任务的结果时。随着任务的增加,代码的缩进越来越多,逻辑也越来越难跟踪,最终形成一团乱麻般的复杂结构。
getData((data) => {
processData(data, (processedData) => {
saveData(processedData, (savedData) => {
notifyUser(savedData, () => {
console.log("所有任务完成!");
});
});
});
});
这里每个任务都依赖于前一个任务,导致多层的缩进和难以跟踪的逻辑。如果在任何一个点上出现问题,正确地处理错误就会变得更加复杂。
为了从回调地狱中解救你,现代 JavaScript 提供了解决方案:
-
Promise — 使代码更加扁平,易于阅读。
-
Async/Await — 简化了异步操作的链式调用,让代码看起来像是同步的。
Promise
JavaScript 中的 Promise 是一个对象,表示异步操作最终完成(或失败)及其结果的状态。它就像你在现实生活中做出的一个 承诺:有些事情可能不会立刻发生,但你要么兑现要么食言。
在 JavaScript 中,Promise 允许你以更干净的方式编写异步代码,避免 回调地狱。Promise 使用 new Promise 语法创建,并接受一个构造函数,有两个参数:resolve(如果任务成功)和 reject(如果任务失败)。
const myPromise = new Promise((resolve, reject) => {
// 模拟一个像获取数据这样的异步任务
const success = true; // 将其改为 false 以模拟失败
setTimeout(() => {
if (success) {
resolve("任务成功完成!"); // 成功
} else {
reject("任务失败!"); // 失败
}
}, 2000); // 两秒延迟
});
这里:
-
Promise 模拟了一个需要两秒的任务。
-
如果任务成功(
success为true),则调用resolve并附带一条消息。 -
如果任务失败(
success为false),则调用reject并附带一条错误信息。
如何处理 Promise
处理 Promise 的结果,我们使用两种方法:
-
.then()用于处理成功的结果(当 Promise 被解决时)。 -
.catch()用于处理错误(当 Promise 被拒绝时)。
myPromise
.then((message) => {
console.log(message); // "任务成功完成!"(如果 resolve 被调用)
})
.catch((error) => {
console.log(error); // "任务失败!"(如果 reject 被调用)
});
Promise 中的链式调用
Promise 允许你使用 .then() 方法来链式调用异步操作,这让代码更加线性并且容易追踪。每个 .then() 都代表了异步流程中的一步。.catch() 方法允许你在 Promise 链的末尾集中处理错误,使代码更有组织。
fetchData()
.then(data => processData1(data))
.then(result1 => processData2(result1))
.then(result2 => processData3(result2))
.catch(error => handleError(error));
通过避免深度嵌套的回调,Promise 促进了更易读和维护的代码结构。链式调用和错误处理机制有助于创建更清晰和更有条理的代码库。
// 回调地狱
fetchData1(data1 => {
processData1(data1, result1 => {
fetchData2(result1, data2 => {
processData2(data2, result2 => {
fetchData3(result2, data3 => {
processData3(data3, finalResult => {
console.log("最终结果:", finalResult);
});
});
});
});
});
});
// 使用 Promise
fetchData1()
.then(result1 => processData1(result1))
.then(data2 => fetchData2(data2))
.then(result2 => processData2(result2))
.then(data3 => fetchData3(data3))
.then(finalResult => {
console.log("最终结果:", finalResult);
})
.catch(error => handleError(error));
finally 方法
finally 方法用于无论 Promise 是成功还是失败都会执行代码。
myPromise
.then((data) => {
console.log("数据:", data);
})
.catch((error) => {
console.error("错误:", error);
})
.finally(() => {
console.log("最后块"); // 不管怎样都会执行
});
如果 Promise 被 解决(即,成功获取数据),.then() 方法将会被执行。另一方面,如果 Promise 遇到了错误,.catch() 方法将会被调用来处理错误。然而,.finally() 方法没有这样的条件——无论 Promise 是解决还是拒绝,它总会被执行。不管是 .then() 还是 .catch() 被触发,.finally() 块肯定会在最后执行。
这个方法对于清理资源、停止加载指示器或者做一些必须在 Promise 完成后做的事情非常有用,不论其结果如何。
JavaScript Promise 方法
JavaScript 提供了几种 Promise 方法,使得处理异步任务更加灵活和强大。这些方法允许你处理多个 Promise,链式调用 Promise 或者以不同的方式处理各种结果。
| 方法 | 描述 |
|---|---|
| all (可迭代对象) | 等待所有的 Promise 解决或任一拒绝。 |
| allSettled (可迭代对象) | 等待所有的 Promise 要么解决要么拒绝。 |
| any (可迭代对象) | 当任一 Promise 解决时返回该 Promise 的值。 |
| race (可迭代对象) | 等待任一 Promise 解决或拒绝。 |
| reject (原因) | 返回一个新的被拒绝的 Promise 对象。 |
| resolve (值) | 返回一个新的被解决的 Promise 对象。 |
Promise.all() - 并行执行
你可以使用 Promise.all() 来并发执行多个异步操作并集体处理它们的结果。
这个方法接受一个 Promise 数组并并行运行它们,返回一个新的 Promise。当所有的输入 Promise 都解决时,这个新的 Promise 也会解决并返回一个包含所有解决值的数组;如果有任何一个输入的 Promise 被拒绝,则新的 Promise 也会被拒绝并带有第一个被拒绝的 Promise 的原因。
const promise1 = Promise.resolve(10);
const promise2 = Promise.resolve(20);
const promise3 = Promise.resolve(30);
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [10, 20, 30]
});
如果其中一个 Promise 被拒绝,整个 Promise.all() 也会被拒绝:
const promise1 = Promise.resolve(10);
const promise2 = Promise.reject("错误!");
Promise.all([promise1, promise2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.log(error); // "错误!"
});
Promise.allSettled()
这个方法接收一个 Promise 数组并在所有 Promise 完成执行后返回一个新的 Promise。与 Promise.all() 不同的是,即使有一个 Promise 失败它也不会失败。相反,它等待所有 Promise 完成并给你一个结果数组,表明每一个是否成功或失败。
当你想知道 每个 Promise 的结果,即使有一些失败了,这也是有用的。
const promise1 = Promise.resolve("成功!");
const promise2 = Promise.reject("失败!");
Promise.allSettled([promise1, promise2]).then((results) => {
console.log(results);
// [{ status: 'fulfilled', value: '成功!' }, { status: 'rejected', reason: '失败!' }]
});
Promise.any()
Promise.any() 接收一个 Promise 数组并返回一个在 任意一个 Promise 解决时 解决的新 Promise。如果所有输入的 Promise 都被拒绝,那么 Promise.any() 会被拒绝,并带有包含所有拒绝原因的 AggregateError。
当你只需要从多个 Promise 中得到 一个成功的结果 时,这是有用的。
const promise1 = fetchData1();
const promise2 = fetchData2();
const promise3 = fetchData3();
Promise.any([promise1, promise2, promise3])
.then((firstFulfilledValue) => {
console.log("第一个解决的 Promise 值为:", firstFulfilledValue);
})
.catch((allRejectedReasons) => {
console.error("所有 Promise 被拒绝的原因:", allRejectedReasons);
});
Promise.race()
这个方法接收一个 Promise 数组并返回一个新的 Promise,该 Promise 在 首个 Promise 完成(解决或拒绝)时解决或拒绝。
当你想要最快的那个 Promise 的结果,忽略其他的时,这是有用的。
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, "First"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200, "Second"));
Promise.race([promise1, promise2]).then((value) => {
console.log(value); // "First" (因为 promise1 首先解决)
});
Promise.resolve()
这个方法 立即解决 一个带有给定值的 Promise。当你已经知道一个 Promise 会解决时,这是有用的。
const resolvedPromise = Promise.resolve("已解决!");
resolvedPromise.then((value) => {
console.log(value); // "已解决!"
});
Promise.reject()
这个方法 立即拒绝 一个带有给定错误或原因的 Promise。当你知道一个 Promise 会失败时,这是有用的。
const rejectedPromise = Promise.reject("已拒绝!");
rejectedPromise.catch((error) => {
console.log(error); // "已拒绝!"
});
希望你在仔细阅读了以上关于 Promise 的解释后不会再对 Promise 有任何困惑。现在,是时候继续学习 Async/Await 了。
Async/Await
async / await 是一种现代的方式来处理 JavaScript 中的异步操作,在 ES2017 (ES8) 中引入。它是基于 Promises 的,但提供了更干净且更易读的语法,使得异步代码看起来和表现得更像是同步代码。这使得理解和调试变得更加容易。
如何使用 async 和 await
当你使用 async 关键字声明一个函数时,它总是返回一个 Promise。即使你不显式地返回一个 Promise,JavaScript 也会将返回值包装在一个已解决的 Promise 中。
await 关键字只能在 async 函数内部使用。它让 JavaScript 等待 直到 Promise 解决后再继续执行下一行代码。它“暂停”了函数的执行,让你可以像处理同步代码一样顺序地处理异步任务。
让我们看一个简单的例子来了解它是如何工作的:
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log("Data:", data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
console.log("Fetch operation complete");
}
}
fetchData();
工作原理:
-
async函数:由于fetchData函数标记为async,这意味着它返回一个 Promise。 -
await fetch():await暂停函数的执行直到由fetch()返回的 Promise 解决。然后在 Promise 解决后继续执行下一行代码。 -
try/catch:我们使用try/catch块来处理任何可能发生的异步操作中的错误。 -
finally:无论成功还是失败,finally块都会被执行。
为什么使用 async / await
使用 async/await 后,你的代码变得更易读且逻辑更自然。这使得在处理多个异步操作时更容易跟踪逻辑。
将其与使用 .then() 处理 Promise 的方式相比:
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => {
console.log("Data:", data);
})
.catch(error => {
console.error("Error fetching data:", error);
})
.finally(() => {
console.log("Fetch operation complete");
});
async/await 版本看起来更干净也更容易理解。async/await 帮助避免了回调或 .then() 链的嵌套,使得你的代码更线性也更容易跟踪。
多个 await 示例
你也可以在不需要链式调用 Promise 的情况下顺序处理多个异步任务:
async function processOrders() {
const user = await getUserDetails(); // 等待用户详情
const orders = await getOrders(user.id); // 等待订单
console.log("Orders:", orders);
}
processOrders();
在这个例子中,函数在进行到下一步之前等待每个任务完成,就像同步代码的行为一样。
使用 Promise.all() 和 async/await 进行并行执行
如果你想同时(并行)执行多个异步操作,你仍然可以结合使用 Promise.all() 和 async/await:
async function getAllData() {
const [user, orders] = await Promise.all([getUserDetails(), getOrders()]);
console.log("User:", user);
console.log("Orders:", orders);
}
这里,getUserDetails() 和 getOrders() 同时运行,并且函数在记录结果之前等待两个都完成。
结论
在 JavaScript 中,处理异步操作随着时间的推移不断演变,提供了不同的工具来使得代码更易于管理和效率更高。回调 是最初的方法,但随着代码复杂性的增加,它们常常导致诸如“回调地狱”的问题。Promises 随后出现,提供了更干净、更结构化的方式来管理异步任务,使用 .then() 和 .catch() 改进了可读性和减少了嵌套。
最终,async/await 作为一种基于 Promises 的现代语法被引入,使得异步代码看起来更像是同步代码。它进一步简化了过程,允许编写更易于阅读且更易于维护的代码。每种技术在 JavaScript 中都有其重要的作用,掌握它们能帮助你写出更高效、清晰且稳健的代码。
理解何时使用每种方法——回调适用于简单任务,Promises 适用于结构化处理,而 async/await 则适用于可读性强、可扩展的异步代码——将赋予你为项目做出最佳选择的能力。
我的最后的话
我过去对 Promise 的概念感到困惑,特别是 Promise 的不同方法。回调对我来说一直是一个很大的挑战,因为语法总是显得非常令人困惑。因此,我在线阅读了各种来源,包括聊天机器人,来整理出这个描述。老实说,聊天机器人并不总是提供直接且易懂的答案。所以,我没有仅仅从不同的地方复制粘贴——我简化了一切,以便它可以作为对我自己以及任何其他对这些概念有困难的人的清晰笔记。我希望这份笔记能让你毫无困惑。
5593

被折叠的 条评论
为什么被折叠?



