详解 JavaScript [你的主题]:概念、用法与实例 – wiki基地


详解 JavaScript Promise:概念、用法与实例

在现代 JavaScript 开发中,异步操作无处不在。从用户界面的事件响应、网络请求(AJAX)、定时器到文件读写,我们都需要处理那些不会立即返回结果的操作。传统上,回调函数 (callbacks) 是处理异步的主要方式,但随着应用复杂度的增加,回调函数容易导致“回调地狱 (Callback Hell)”或“末日金字塔 (Pyramid of Doom)”,使得代码难以阅读、维护和调试。JavaScript Promise 的出现,正是为了解决这些问题,提供一种更优雅、更强大的异步编程模式。

本文将深入探讨 JavaScript Promise,从基本概念、核心用法、高级技巧到实际应用案例,帮助你全面掌握这一现代 JavaScript 的基石。

一、什么是 Promise?

1. 概念与隐喻

Promise,顾名思义,是一个“承诺”。你可以把它想象成现实生活中的一个凭证或约定:

  • 餐厅点餐的票据:你点了一份餐(发起一个异步操作),餐厅给了你一张票据(一个 Promise 对象)。你拿着这张票据,可以去做其他事情,不必一直守在柜台。当餐品准备好(异步操作成功)或者因为某些原因无法制作(异步操作失败)时,餐厅会通知你凭票据来取餐或告知你原因。
  • 未来的值:一个 Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。它是一个代理,用于表示一个在创建 Promise 时不一定已知的值。

2. Promise 的状态

一个 Promise 对象必然处于以下三种状态之一:

  • Pending (等待中/进行中):初始状态,既不是成功,也不是失败状态。这是 Promise 创建时的默认状态,表示异步操作尚未完成。
  • Fulfilled (已成功/已兑现):表示异步操作已成功完成。当 Promise 状态变为 fulfilled 时,会有一个“值 (value)”与之关联,这个值就是异步操作的结果。
  • Rejected (已失败/已拒绝):表示异步操作已失败。当 Promise 状态变为 rejected 时,会有一个“原因 (reason)”与之关联,这个原因通常是一个 Error 对象,解释了失败的缘由。

关键特性:

  • 状态一旦改变,就不会再变:一个 Promise 从 pending 状态可以转变为 fulfilled 或 rejected 状态,但一旦转变,其状态就固定了,不能再变回 pending,也不能从 fulfilled 变为 rejected,反之亦然。
  • 不可变性:Promise 的状态是内部的,外部代码无法直接修改。只能通过 resolvereject 函数来改变其状态。

二、Promise 的基本用法

1. 创建 Promise

我们使用 new Promise() 构造函数来创建一个 Promise 实例。构造函数接收一个执行器函数 executor 作为参数。这个 executor 函数会立即执行,并接收两个参数:resolvereject

  • resolve(value): 当异步操作成功时,调用此函数,并将 Promise 的状态从 pending 变为 fulfilled,同时将异步操作的结果 value 传递出去。
  • reject(reason): 当异步操作失败时,调用此函数,并将 Promise 的状态从 pending 变为 rejected,同时将失败的原因 reason (通常是一个 Error 对象) 传递出去。

“`javascript
const myPromise = new Promise((resolve, reject) => {
// 执行异步操作
console.log(“Promise 执行器函数开始执行…”);
setTimeout(() => {
const success = Math.random() > 0.5; // 模拟成功或失败
if (success) {
resolve(“操作成功!数据已获取。”); // 操作成功,状态变为 fulfilled
} else {
reject(new Error(“操作失败!服务器错误。”)); // 操作失败,状态变为 rejected
}
}, 2000); // 模拟一个耗时2秒的异步操作
});

console.log(“Promise 已创建,当前状态为 pending。”);
“`

在上面的例子中,myPromise 在创建后立即进入 pending 状态。2秒后,根据 Math.random() 的结果,它会调用 resolvereject,从而改变状态并传递相应的值或原因。

2. 消费 Promise:then(), catch(), finally()

Promise 创建后,我们需要一种方式来处理其最终的结果(成功的值或失败的原因)。这通过 Promise 实例的 then, catch, 和 finally 方法实现。

  • then(onFulfilled, onRejected)
    then 方法接收两个可选的回调函数作为参数:

    • onFulfilled: 当 Promise 状态变为 fulfilled 时被调用,接收成功的值作为参数。
    • onRejected: 当 Promise 状态变为 rejected 时被调用,接收失败的原因作为参数。

    javascript
    myPromise.then(
    (value) => {
    console.log("Promise 成功:", value); // 如果 myPromise fulfilled
    },
    (reason) => {
    console.error("Promise 失败 (then 的第二个参数):", reason.message); // 如果 myPromise rejected
    }
    );

  • catch(onRejected)
    catch 方法实际上是 then(null, onRejected) 的语法糖,专门用于处理 Promise 被拒绝 (rejected) 的情况。它只接收一个回调函数,当 Promise 状态变为 rejected 时被调用。

    javascript
    myPromise.catch((reason) => {
    console.error("Promise 失败 (catch):", reason.message); // 如果 myPromise rejected
    });

    通常,我们更倾向于使用 .catch() 来处理错误,因为它更具可读性,并且可以捕获其前面 then 链中任何未处理的拒绝。

  • finally(onFinally)
    finally 方法接收一个回调函数,无论 Promise 最终是 fulfilled 还是 rejected,这个回调函数都会被执行。它通常用于执行一些清理工作,比如关闭加载指示器、释放资源等。
    finally 回调不接收任何参数,因为它不知道 Promise 是成功还是失败。finally 返回一个新的 Promise,这个 Promise 的状态和值(或原因)通常与其调用的 Promise 相同。如果 onFinally 回调返回一个 rejected 的 Promise 或者抛出错误,则 finally 返回的 Promise 会以这个新的原因被拒绝。

    javascript
    myPromise
    .then((value) => {
    console.log("成功处理:", value);
    })
    .catch((reason) => {
    console.error("错误处理:", reason.message);
    })
    .finally(() => {
    console.log("Promise 处理完毕,执行清理操作。");
    });

三、Promise 链 (Promise Chaining)

Promise 最强大的特性之一是能够将多个异步操作链接起来,形成一个清晰的、顺序的流程。这是通过 then()catch() 方法实现的,因为 它们总是返回一个新的 Promise 对象

  1. 基本链式调用
    前一个 then 回调的返回值会作为下一个 then 回调的输入。

    “`javascript
    function step1() {
    return new Promise((resolve) => {
    setTimeout(() => {
    console.log(“步骤1完成”);
    resolve(10);
    }, 1000);
    });
    }

    function step2(dataFromStep1) {
    return new Promise((resolve) => {
    setTimeout(() => {
    console.log(“步骤2完成,接收到数据:”, dataFromStep1);
    resolve(dataFromStep1 * 2);
    }, 1000);
    });
    }

    function step3(dataFromStep2) {
    return new Promise((resolve) => {
    setTimeout(() => {
    console.log(“步骤3完成,接收到数据:”, dataFromStep2);
    resolve(dataFromStep2 + 5);
    }, 1000);
    });
    }

    step1()
    .then((result1) => {
    // result1 是 10
    return step2(result1); // 返回一个新的 Promise
    })
    .then((result2) => {
    // result2 是 20 (10 * 2)
    return step3(result2); // 返回一个新的 Promise
    })
    .then((finalResult) => {
    // finalResult 是 25 (20 + 5)
    console.log(“所有步骤完成,最终结果:”, finalResult);
    })
    .catch((error) => {
    console.error(“链式调用中发生错误:”, error);
    });
    “`

  2. then 中返回 Promise
    如果 then 的回调函数返回一个 Promise,那么后续的 then 会等待这个新的 Promise 完成,并接收其解析值。这使得我们可以将异步操作扁平化,避免嵌套。

  3. 错误处理
    在 Promise 链中,任何一个 Promise 被拒绝(或在 then 回调中抛出错误),控制流会跳过后续的 thenonFulfilled 回调,直接进入链中最近的 catch 处理程序或 thenonRejected 回调。

    javascript
    new Promise((resolve, reject) => {
    console.log("开始");
    reject(new Error("第一次就失败"));
    })
    .then(() => {
    console.log("这个 then 不会执行");
    })
    .catch((error) => {
    console.error("捕获到错误:", error.message); // 输出: 捕获到错误: 第一次就失败
    // 可以在这里处理错误,并选择性地恢复流程
    return "从错误中恢复的值"; // 返回一个普通值,使后续 .then 执行
    // throw new Error("新的错误"); // 或者抛出新错误,给后续 .catch 处理
    })
    .then((value) => {
    console.log("上一个 catch 恢复后的值:", value); // 输出: 上一个 catch 恢复后的值: 从错误中恢复的值
    });

四、Promise 的静态方法

Promise 构造函数本身也提供了一些有用的静态方法:

  1. Promise.resolve(value):
    返回一个立即以给定值 value 解析的 Promise 对象。

    • 如果 value 本身就是一个 Promise,则返回这个 Promise。
    • 如果 value 是一个 thenable 对象(即拥有 then 方法的对象),Promise.resolve 会尝试展开这个 thenable 对象,并采用其最终状态。
    • 否则,返回的 Promise 将以 value 成功兑现。

    “`javascript
    Promise.resolve(“立即成功”).then(val => console.log(val)); // 立即成功

    const p1 = new Promise(resolve => setTimeout(() => resolve(“p1 resolved”), 100));
    Promise.resolve(p1).then(val => console.log(val)); // 等待 p1 完成后输出 “p1 resolved”
    “`

  2. Promise.reject(reason):
    返回一个立即以给定原因 reason 拒绝的 Promise 对象。

    javascript
    Promise.reject(new Error("立即失败")).catch(err => console.error(err.message)); // 立即失败

  3. Promise.all(iterable):
    接收一个 Promise 数组(或任何可迭代对象)作为参数。

    • 当 iterable 中的所有 Promise 都成功完成 (fulfilled) 时,Promise.all 返回的 Promise 才会成功完成。其成功值是一个数组,包含了 iterable 中每个 Promise 的成功值,且顺序与原始 iterable 中的 Promise 顺序一致。
    • 如果 iterable 中的任何一个 Promise 被拒绝 (rejected),Promise.all 返回的 Promise 会立即被拒绝,并带有第一个被拒绝的 Promise 的原因。

    “`javascript
    const promise1 = Promise.resolve(3);
    const promise2 = 42; // 普通值会被 Promise.resolve() 包装
    const promise3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, ‘foo’);
    });

    Promise.all([promise1, promise2, promise3]).then((values) => {
    console.log(values); // 输出: [3, 42, “foo”]
    });

    const promiseFail = Promise.reject(new Error(“失败了”));
    Promise.all([promise1, promiseFail, promise3])
    .then(values => console.log(“这不会执行”))
    .catch(error => console.error(“Promise.all 失败:”, error.message)); // 输出: Promise.all 失败: 失败了
    “`

  4. Promise.allSettled(iterable): (ES2020)
    接收一个 Promise 数组。它会等待所有给定的 Promise 都已经 settled (无论是 fulfilled 还是 rejected)。
    返回的 Promise 会在所有输入的 Promise 都 settled 后 fulfilled。其成功值是一个对象数组,每个对象描述了对应 Promise 的结果,格式为:

    • {status: "fulfilled", value: value} (如果成功)
    • {status: "rejected", reason: reason} (如果失败)

    这与 Promise.all 不同,Promise.allSettled 不会因为一个 Promise 失败而短路。

    “`javascript
    const pSuccess = Promise.resolve(“成功”);
    const pFail = Promise.reject(new Error(“失败”));
    const pPending = new Promise(resolve => setTimeout(() => resolve(“延迟成功”), 500));

    Promise.allSettled([pSuccess, pFail, pPending])
    .then((results) => {
    results.forEach(result => {
    if (result.status === “fulfilled”) {
    console.log(Fulfilled: ${result.value});
    } else {
    console.error(Rejected: ${result.reason.message});
    }
    });
    });
    // 输出 (顺序可能不同,取决于pPending完成时间,但通常是按输入顺序):
    // Fulfilled: 成功
    // Rejected: 失败
    // Fulfilled: 延迟成功 (在500ms后)
    “`

  5. Promise.race(iterable):
    接收一个 Promise 数组。返回一个新的 Promise,这个 Promise 会在 iterable 中的任何一个 Promise settled (无论是 fulfilled 还是 rejected) 时立即以相同的状态和值/原因 settled。
    就像一场赛跑,谁第一个到达终点(settled),Promise.race 就采用谁的结果。

    “`javascript
    const pRace1 = new Promise((resolve) => setTimeout(() => resolve(“一号选手到达”), 100));
    const pRace2 = new Promise((resolve, reject) => setTimeout(() => reject(new Error(“二号选手摔倒”)), 50));

    Promise.race([pRace1, pRace2])
    .then(value => console.log(“Race 胜者:”, value))
    .catch(error => console.error(“Race 失败者:”, error.message));
    // 输出: Race 失败者: 二号选手摔倒 (因为 pRace2 更快 settled)
    “`

  6. Promise.any(iterable): (ES2021)
    接收一个 Promise 数组。返回一个新的 Promise,这个 Promise 会在 iterable 中的任何一个 Promise fulfilled 时立即以该 Promise 的值 fulfilled。
    如果 iterable 中所有的 Promise 都 rejected,则 Promise.any 返回的 Promise 会以一个 AggregateError 对象 rejected,该对象有一个 errors 属性,是一个包含所有拒绝原因的数组。

    “`javascript
    const pAny1 = Promise.reject(new Error(“第一个失败”));
    const pAny2 = new Promise(resolve => setTimeout(() => resolve(“第二个成功”), 200));
    const pAny3 = new Promise(resolve => setTimeout(() => resolve(“第三个成功,但较慢”), 500));

    Promise.any([pAny1, pAny2, pAny3])
    .then(value => console.log(“Any 成功:”, value)) // 输出: Any 成功: 第二个成功
    .catch(error => console.error(“Any 失败:”, error));

    const allReject = [
    Promise.reject(new Error(“err1”)),
    Promise.reject(new Error(“err2”))
    ];
    Promise.any(allReject)
    .catch(aggError => {
    console.error(“Promise.any 全部失败:”, aggError.message);
    console.error(“具体错误:”, aggError.errors.map(e => e.message));
    // 输出: Promise.any 全部失败: All promises were rejected
    // 具体错误: [ ‘err1’, ‘err2’ ]
    });
    “`

五、Promise 与 Async/Await

ES2017 (ES8) 引入了 async 函数和 await 表达式,它们是构建在 Promise 之上的语法糖,使得异步代码的编写和阅读更像同步代码。

  • async 函数:
    在函数声明前加上 async 关键字,表示该函数是一个异步函数。
    async 函数总是隐式地返回一个 Promise。如果函数内部返回一个非 Promise 值,这个值会被 Promise.resolve() 包装成一个 fulfilled 的 Promise。如果函数内部抛出错误,这个错误会被 Promise.reject() 包装成一个 rejected 的 Promise。

  • await 表达式:
    await 关键字只能在 async 函数内部使用。
    await 后面跟着一个 Promise 时,它会暂停 async 函数的执行,直到该 Promise settled。

    • 如果 Promise fulfilled,await 表达式的结果就是 Promise 的成功值。
    • 如果 Promise rejected,await 表达式会抛出 Promise 的拒绝原因 (通常是一个 Error 对象)。

示例对比:

假设我们有一个模拟获取用户数据和帖子数据的函数:

``javascript
function getUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(
正在获取用户 ${userId} 的数据…`);
if (userId === 1) {
resolve({ id: 1, name: “Alice” });
} else {
reject(new Error(“用户未找到”));
}
}, 1000);
});
}

function getPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(正在获取用户 ${userId} 的帖子...);
resolve([
{ id: 101, title: “Promise 简介” },
{ id: 102, title: “Async/Await 详解” }
]);
}, 1500);
});
}

// 使用 Promise.then 链
function fetchUserDataThen(userId) {
getUser(userId)
.then(user => {
console.log(“用户信息:”, user);
return getPosts(user.id); // 返回新的 Promise
})
.then(posts => {
console.log(“用户帖子:”, posts);
})
.catch(error => {
console.error(“获取数据失败 (then 链):”, error.message);
});
}

fetchUserDataThen(1);
// fetchUserDataThen(2); // 测试错误情况

// 使用 async/await
async function fetchUserDataAsyncAwait(userId) {
try {
console.log(“Async/Await: 开始获取数据”);
const user = await getUser(userId); // 等待 getUser 完成
console.log(“Async/Await – 用户信息:”, user);

const posts = await getPosts(user.id); // 等待 getPosts 完成
console.log("Async/Await - 用户帖子:", posts);
console.log("Async/Await: 所有数据获取完毕");
return { user, posts }; // async 函数返回的 Promise 将以此对象 fulfilled

} catch (error) {
console.error(“获取数据失败 (async/await):”, error.message);
throw error; // 或者重新抛出,让调用者处理
}
}

fetchUserDataAsyncAwait(1)
.then(data => console.log(“Async/Await 最终结果:”, data))
.catch(err => console.error(“Async/Await 外部捕获:”, err.message));

// fetchUserDataAsyncAwait(2)
// .then(data => console.log(“Async/Await 最终结果:”, data))
// .catch(err => console.error(“Async/Await 外部捕获:”, err.message));
“`

async/await 使得异步代码的结构更扁平,逻辑更清晰,错误处理也更直观(使用标准的 try...catch 语句)。

六、Promise 的最佳实践与注意事项

  1. 总是返回 Promise:如果一个函数执行异步操作,它应该返回一个 Promise。
  2. 总是处理拒绝 (Rejection):确保每个 Promise 链都有一个 .catch() 来处理潜在的错误,或者在 async 函数中使用 try...catch。未处理的 Promise 拒绝(unhandled promise rejections)可能导致 Node.js 进程崩溃或在浏览器中产生难以追踪的错误。
  3. 避免 “Promise 地狱”:虽然 Promise 解决了回调地狱,但如果不正确使用,也可能产生深层嵌套的 .then()。通常通过在 .then() 中返回新的 Promise 来保持链的扁平。
  4. 理解 Promise.all vs Promise.allSettled:根据需求选择。如果一个失败就意味着整体失败,用 Promise.all。如果需要所有操作的结果,无论成败,用 Promise.allSettled
  5. async/await 不是万能的:虽然 async/await 提高了可读性,但它本质上是顺序执行 await 后面的 Promise。如果多个异步操作可以并行执行,应该使用 Promise.all 配合 await,而不是多个独立的 await 语句。
    “`javascript
    // 不好的并行(实际是串行)
    async function badParallel() {
    const data1 = await fetchData1();
    const data2 = await fetchData2(); // fetchData2 等待 fetchData1 完成
    return { data1, data2 };
    }

    // 好的并行
    async function goodParallel() {
    const [data1, data2] = await Promise.all([
    fetchData1(), // fetchData1 和 fetchData2 同时开始
    fetchData2()
    ]);
    return { data1, data2 };
    }
    ``
    6. **不要在
    Promise构造函数中使用async**:new Promise(async (resolve, reject) => { … })是一种反模式。执行器函数本身应该是同步的,它负责启动异步操作。如果执行器函数是async的,它会返回一个 Promise,但Promise构造函数期望的是一个立即执行的同步函数。
    7. **谨慎使用
    Promise.race`**:它的主要用途是处理超时或选择最快响应的资源,但要注意,一旦一个 Promise settled,其他 Promise 的结果就会被忽略。

七、实际案例:使用 Promise 封装 XMLHttpRequest

在 Fetch API 普及之前,XMLHttpRequest (XHR) 是进行 AJAX 请求的主要方式。我们可以用 Promise 封装它:

“`javascript
function getJSON(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(‘GET’, url);
xhr.responseType = ‘json’; // 自动解析 JSON

xhr.onload = () => {
  if (xhr.status >= 200 && xhr.status < 300) {
    resolve(xhr.response); // 请求成功
  } else {
    reject(new Error(`请求失败,状态码: ${xhr.status} ${xhr.statusText}`));
  }
};

xhr.onerror = () => {
  reject(new Error("网络错误或请求无法发送")); // 网络层面的错误
};

xhr.send();

});
}

// 使用封装后的 getJSON
getJSON(‘https://jsonplaceholder.typicode.com/todos/1’)
.then(data => {
console.log(“获取到的TODO:”, data);
})
.catch(error => {
console.error(“获取TODO失败:”, error.message);
});

// 使用 async/await
async function fetchTodo() {
try {
const todo = await getJSON(‘https://jsonplaceholder.typicode.com/todos/2’);
console.log(“Async/Await 获取到的TODO:”, todo);
} catch (error) {
console.error(“Async/Await 获取TODO失败:”, error.message);
}
}
fetchTodo();
``
现代浏览器和 Node.js 推荐使用
fetch` API,它本身就返回 Promise,更为简洁。

八、总结

Promise 是 JavaScript 异步编程的基石。它通过引入状态机和链式调用的概念,极大地改善了回调函数带来的可读性和可维护性问题。Promise 使得复杂的异步流程管理变得更加清晰和结构化。

核心要点回顾:

  • Promise 代表一个异步操作的最终结果,有 pending, fulfilled, rejected 三种状态。
  • 使用 new Promise(executor) 创建,通过 resolvereject 改变状态。
  • 使用 .then(), .catch(), .finally() 消费 Promise 的结果。
  • .then().catch() 返回新的 Promise,支持链式调用。
  • Promise.all(), Promise.allSettled(), Promise.race(), Promise.any() 等静态方法提供了强大的并发控制能力。
  • async/await 是 Promise 的语法糖,让异步代码更像同步代码,提高了可读性。

深入理解和熟练运用 Promise 及其相关模式,对于编写高质量、可维护的现代 JavaScript 应用至关重要。随着 JavaScript 语言的不断发展,Promise 及其生态系统(如 async/await)将继续在异步编程领域扮演核心角色。希望本文能助你彻底掌握 Promise,在异步编程的道路上更加游刃有余。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部