在 JavaScript 中,Promise 是一个表示异步操作最终完成或失败的对象。它本质上是一个返回的对象,你可以附加回调函数,而不是将回调传递给函数。
let promise = new Promise((resolve, reject) => {
let condition = true; // 这可以是某个操作的结果
// 1秒后检查条件并解决或拒绝 Promise
setTimeout(() => {
if (condition) {
resolve('Promise 已完成!');
} else {
reject('Promise 被拒绝!');
}
}, 1000);
});
// 将 then() 和 catch() 处理程序附加到 Promise
promise
.then(value => {
// 如果 Promise 已解决,则执行此操作
console.log(value); // 输出: Promise 已完成!
})
.catch(error => {
// 如果 Promise 被拒绝,则执行此操作
console.log(error);
});
在这个示例中,创建了一个 Promise,它将在 1 秒后解决或拒绝,具体取决于条件的值。如果 Promise 成功,将调用 resolve 函数;如果 Promise 失败,将调用 reject 函数。
当 Promise 解决时,then 方法被调用,并接收传递给 resolve 函数的值。类似地,当 Promise 被拒绝时,catch 方法被调用,并接收传递给 reject 函数的值。
setTimeout 函数在这个问题中发挥了关键作用。它是一个方法,调用一个函数或在指定的毫秒数后评估一个表达式。在 JavaScript 中,setTimeout 用于延迟代码的执行。
console.log("启动定时器...");
setTimeout(() => {
console.log("定时已完成!");
}, 2000);
在这个示例中,“启动定时器…”将立即记录到控制台。然后,调用 setTimeout 函数,传递两个参数:一个回调函数和以毫秒为单位的延迟。回调函数是一个简单的箭头函数,用于将 “定时已完成!”记录到控制台,延迟为 2000 毫秒(或2秒)。
一旦调用了 setTimeout,JavaScript 运行时设置了定时器,但随后立即继续执行任何后续代码。它不会暂停或等待定时器完成,这展示了 JavaScript 的非阻塞特性。
在指定的延迟之后(在本例中为2秒),将回调函数添加到任务队列中。但重要的是要注意,回调函数不一定会在此刻立即执行。回调函数实际执行之前的实际延迟可能会比指定的延迟稍长。这是由于事件驱动的 JavaScript 运行时和单线程事件循环的性质所决定的。
假设主 JavaScript 线程中有一个耗时较长的进程或操作。在这种情况下,即使定时器在后台完成,回调函数仍必须等待阻塞任务的完成。这是因为事件循环一次只能处理一个任务,并按照排队的顺序处理任务。
因此,setTimeout 中指定的“2 秒”应被理解为在调用回调函数之前的 “最小延迟”,而不是 “保证延迟”。如果 JavaScript 运行时正忙于其他任务,回调函数实际执行的时间可能会超过 2 秒。这种行为强调了理解 JavaScript 异步性质的重要性,因为它对代码的性能和行为产生重大影响。
另外,值得一提的是 clearTimeout,这是 JavaScript 定时器函数套件中的一个有用函数。clearTimeout 是一个函数,它取消了先前通过调用 setTimeout 建立的定时器。
下面是它的使用示例:
console.log("启动定时器...");
// setTimeout 返回一个 Timeout 对象,可用于引用定时器
let timeoutId = setTimeout(() => {
console.log("定时已完成!");
}, 2000);
// 一些条件或逻辑
if (/* 一些条件 */) {
// 取消定时器
clearTimeout(timeoutId);
}
如果 if 语句内部的条件为真,那么 clearTimeout 函数将取消通过 setTimeout 设置的定时器。如果取消了定时器,setTimeout 提供的函数将不会被调用。
这在各种场景中很有用,例如,如果要在操作执行之前检查用户是否仍然活跃在页面上,但用户在延迟结束之前导航到其他页面,你可以使用 clearTimeout 来取消检查。
JavaScript 使用调用堆栈来管理函数的执行。当调用函数时,它会被添加到堆栈中。当函数完成时,它会从堆栈中移除。由于 JavaScript 是单线程的,一次只能执行一个函数。
然而,如果一个函数需要较长时间才能执行(例如网络请求),这可能会有问题。这就是事件循环的用武之地。
事件循环 是一个持续的循环,检查调用堆栈是否为空。如果为空,它会从任务队列(也称为事件队列或回调队列)中获取第一个任务并将其推送到调用堆栈中,立即执行它。
setTimeout 是 JavaScript 中的异步函数示例。当调用 setTimeout 函数时,它会启动一个定时器,然后立即返回,允许 JavaScript 运行时在等待定时器完成的同时继续执行其他代码。这是 JavaScript 的非阻塞特性。
一旦定时器完成,给 setTimeout 提供的回调函数将添加到任务队列中。事件循环不断检查调用堆栈和任务队列。当调用堆栈为空时,它会从任务队列中获取第一个任务并将其推送到调用堆栈中以执行。
以下是 JavaScript 如何处理并发操作的方式:
这个过程继续进行,事件循环会在调用堆栈为空时将任务从任务队列中推送到调用堆栈,从而使 JavaScript 能够处理多个操作,尽管它是单线程的。
这是 JavaScript 处理异步操作的高级概述。实际上更复杂,还涉及微任务和宏任务等附加特性,但这是其基本概念。查看事件循环详解。
Async/await 可以被看作是 Promise 的语法糖,它使异步代码更易于编写和理解。当我们使用 async 关键字标记函数时,它将成为一个自动返回 promise 的异步函数。在异步函数中,我们可以使用 await 关键字暂停代码的执行,直到 promise 解决或拒绝。
通过使用 await,我们可以消除使用 promises 时通常需要的明确的 .then() 和 .catch() 链。相反,我们可以以更线性和类似同步代码的方式构建代码。这使得更容易理解程序的流程并以更简洁的方式处理错误。
// 使用 promises 和显式的 .then() 和 .catch()
fetchData()
.then(response => {
// 处理响应
console.log("响应:", response);
return processData(response);
})
.then(result => {
// 处理处理后的数据
console.log("处理后的数据:", result);
})
.catch(error => {
// 处理任何错误
console.error("错误:", error);
});
// 使用 async/await
async function fetchDataAndProcess() {
try {
const response = await fetchData();
console.log("响应:", response);
const result = await processData(response);
console.log("处理后的数据:", result);
} catch (error) {
console.error("错误:", error);
}
}
fetchDataAndProcess();
通过使用明确的 .then() 和 .catch() 链,我们必须分别处理异步操作的每个步骤。当涉及多个 promises 时,可能会变得复杂,从而导致嵌套或链接的 .then() 调用。此外,错误处理需要单独的 .catch() 块。
相比之下,第二个示例使用 async/await 在更线性和类似同步代码的方式中构建代码。fetchDataAndProcess() 函数标记为 async,允许我们在其中使用 await 关键字。这消除了明确的 .then() 和 .catch() 链的需要。
在底层,await 关键字会暂停函数的执行,使其他任务继续执行,比如处理用户输入或动画。JavaScript 引擎会切换到执行其他代码,直到由异步函数返回的 promise 解决,然后它将恢复异步函数中的其余代码的执行。
Promise 链式编程是 JavaScript 中的一种技术,允许你按顺序执行多个异步操作,每个操作在前一个操作完成后启动。Promise 链的主要优点是,它允许你避免使用嵌套回调来处理异步代码时的 “回调地狱” 或 “回调金字塔”。相反,可以编写几乎看起来像同步代码的异步代码,从而更容易理解和维护。Promise 链中的每个 then 方法都会接收上一个 promise 解决的结果。此结果可以用来通知链中的下一个步骤。如果链中的 promise 被拒绝,后续的 then 方法将被跳过,直到找到 catch 方法来处理错误。
fetchData()
.then(response => {
console.log("响应:", response);
return processData(response); // 这返回一个新的 promise
})
.then(processedData => {
console.log("处理后的数据:", processedData);
return furtherProcessing(processedData); // 这返回另一个新的 promise
})
.then(finalResult => {
console.log("最终结果:", finalResult);
})
.catch(error => {
console.error("错误:", error);
});
fetchData、processData 和 furtherProcessing 都是异步函数,返回 promises。then 方法被链接在一起,每个 then 在前一个 promise 解决后开始其操作。如果链中的任何 promise 被拒绝,将调用最后的 catch 方法来处理错误。
在 JavaScript 中,Promises 提供了处理异步操作及其结果的几种强大方法之一就是 .finally 方法。.finally 方法是 Promise 的内置方法,它始终会执行,无论 promise 是否被解决。这使得它成为放置无论 promise 结果如何都必须运行的清理代码的绝佳位置。
let isLoading = true;
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('错误:', error))
.finally(() => {
isLoading = false;
console.log('获取操作完成');
});
使用 fetch(返回一个 promise)从 URL 获取数据。然后,我们使用 .then 处理响应,并使用 .catch 处理任何错误。最后,无论获取操作是否成功,都会调用 .finally 以将 isLoading 设置为 false 并在控制台上记录一条消息。
在解决这个问题时,可能会派上用场的一个有趣事实是,在异步函数中,无论你是返回 return new Promise() 还是 return await new Promise(),行为通常是相同的。这是因为异步函数始终将返回值包装在 promise 中。然而,在某些情况下,如错误处理,使用 await 可能会有所不同。
思考下面的示例:
async function example() {
try {
return new Promise((resolve, reject) => {
throw new Error('糟糕!');
});
} catch (err) {
console.error(err);
}
}
example(); // 错误不会被捕获,它拒绝了 example 返回的 promise。
async function example2() {
try {
return await new Promise((resolve, reject) => {
throw new Error('糟糕!');
});
} catch (err) {
console.error(err);
}
}
example2(); // 错误被捕获,example2 返回的 promise 被解决。
在 example 函数中,try 块不会捕获 promise 引发的错误,因为 promise 在引发错误之前就已经返回了。在 example2 中,await 会导致函数等待 promise 完成或抛出错误,因此它可以捕获 promise 引发的错误。