Promise详细解析

Promise没出现前

在进行异步编程的时候 常使用回调函数的形式进行异步编程,回调函数层层嵌套就会出现 回调地狱

javascript 复制代码
// 模拟一个异步操作:延迟执行并调用回调
function asyncTask(name, delay, callback) {
  setTimeout(() => {
    console.log(`完成任务: ${name}`)
    callback()
  }, delay)
}

回调地狱 是指 多层嵌套回调函数 的情况,通常表现为:

javascript 复制代码
fs.readFile('user.json', (err, userData) => {
  if (err) {
    console.error('读取用户信息失败:', err);
  } else {
    const userId = JSON.parse(userData).id;
    fs.readFile('posts.json', (err, postsData) => {
      if (err) {
        console.error('读取文章列表失败:', err);
      } else {
        const firstPostId = JSON.parse(postsData)[0].id;
        fs.readFile('comments.json', (err, commentsData) => {
          if (err) {
            console.error('读取评论失败:', err);
          } else {
            console.log('评论内容:', JSON.parse(commentsData));
          }
        });
      }
    });
  }
});
  • 可读性极差:代码横向发展,形成一个丑陋的">"形状,逻辑嵌套混乱,难以阅读和理解。
  • 难以维护:如果想在中间增加一个步骤,或者修改某个步骤的逻辑,简直是一场灾难。
  • 错误处理复杂每个异步操作都需要单独处理错误(if (err)),代码冗余,而且很容易遗漏。
  • 控制流混乱:代码的执行顺序与书写顺序完全不一致,违背了人类的线性思维习惯。

promise出现之后

现在,我们用 Promise 来重写上面的逻辑。假设我们已经有了返回 Promise 的 readFilePromise 函数。

typescript 复制代码
readFilePromise('user.json')
  .then(userData => {
    const userId = JSON.parse(userData).id;
    return readFilePromise('posts.json'); // 返回一个新的 Promise
  })
  .then(postsData => {
    const firstPostId = JSON.parse(postsData)[0].id;
    return readFilePromise('comments.json'); // 再次返回一个新的 Promise
  })
  .then(commentsData => {
    console.log('评论内容:', JSON.parse(commentsData));
  })
  .catch(error => {
    // 统一的错误处理
    console.error('在某个环节出错了:', error);
  });

看看 Promise 带来了什么革命性的改变:

  • 可读性极高 :代码从横向的嵌套,变成了纵向的链式调用 ( .then ) 。逻辑清晰,一目了然,完全符合"做完A,再做B,再做C"的线性思维。
  • 易于维护:想在中间加一个步骤?只需要在链中插入一个新的 .then() 即可。
  • 统一的错误处理 :链中任何一个 Promise 失败(rejected),都会被直接传递到最后的 .catch() 中进行统一处理,无需在每一步都写 if (err)。这让错误处理变得异常简洁和健壮。
  • 状态管理:Promise 自身的状态机制(Pending, Fulfilled, Rejected)使得异步操作的管理变得更加规范和可预测。

Promise 是一个 JavaScript 对象,它代表了一个异步操作的最终完成(或失败)及其结果值。 它是一个容器,用于存放一个未来才会知道结果的事件(通常是异步操作)。

它有三个状态 pendding fulfilled rejected,一旦状态从pendding变成fulfilled 或 rejected之后,状态就不能再改变。

当pedding变成fulfilled之后,对应的.then方法注册的onfulfilled回调函数会执行,并返回一个新的promise(称为promise2)。prmose2的状态和值取决于onfulfilled回调函数的返回值。

场景 1:回调函数返回一个普通值 (非 Promise)

如果你在 onFulfilled 中返回一个数字、字符串、对象等,那么 promise2 会立即变为 fulfilled 状态,并且这个返回值会成为 promise2 的结果。

场景 2:回调函数返回一个新的 Promise (链式调用的核心)

如果你在 onFulfilled 中返回了另一个 Promise (我们称之为 promise3),那么 promise2 的状态将不会 立即确定。 相反,promise2 会"接管" promise3 的状态。也就是说,promise2 会等待 promise3 尘埃落定:

  • 如果 promise3 成功了,promise2 也会成功,并且 promise3 的结果会成为 promise2 的结果。
  • 如果 promise3 失败了,promise2 也会失败,并且 promise3 的失败原因会成为 promise2 的失败原因。
场景 3:回调函数中抛出 (throw) 了一个错误

如果你在 onFulfilled 或 onRejected 中 throw 了一个错误,那么 promise2 会立即变为 rejected 状态,并且这个抛出的错误会成为 promise2 的失败原因。

当pedding变成rejected之后,then方法注册的onrejected回调函数会执行,并返回一个promise(称为promise2)。prmose2的状态和值取决于onrejected回调函数的返回值。

场景 1:返回一个普通值 (非 Promise) - "错误恢复"

这是最常见的错误处理场景。当你在 onRejected 回调中返回一个普通值 (或 undefined),就意味着你已经成功地处理并"消化"了这个错误

  • 结果 :.then() 返回的新 Promise (promise2) 将会变为 fulfilled (成功) 状态,并且你返回的那个普通值,会成为 promise2 的成功结果。
javascript 复制代码
const initialPromise = Promise.reject(new Error("API 请求失败"));
initialPromise
  .then(
    (result) => {
      // onFulfilled: 不会执行
      console.log("成功了:", result);
    },
    (error) => {
      // onRejected: 执行
      console.error("在 then 中捕获到错误:", error.message);
      // 处理错误,并返回一个默认值来"救场"
      return { data: "来自本地缓存的默认数据" }; 
    }
  )
  .then(
    (nextResult) => {
      // 这个 then 的 onFulfilled 会执行!
      console.log("后续操作成功:", nextResult);
      // 输出: 后续操作成功: { data: "来自本地缓存的默认数据" }
    },
    (nextError) => {
      // 这个 then 的 onRejected 不会执行
      console.error("后续操作失败:", nextError);
    }
  );

发生了什么?

  • initialPromise 是 rejected 状态。
  • 第一个 .then() 的 onRejected 被调用。
  • onRejected 返回了一个普通对象。
  • 第一个 .then() 方法本身返回了一个新的、成功的 Promise,其结果就是那个普通对象。
  • 因此,第二个 .then() 的 onFulfilled 被调用。 这个机制就是"错误恢复"。它允许你在捕获错误后,让 Promise 链从失败状态恢复到成功状态,继续执行后续的 ****.then()
场景 2:抛出 (throw) 一个新的错误 - "错误传递"

如果你在 onRejected 回调中处理完错误后,又 throw 了一个新的错误,就意味着你想让这个失败状态继续向下传播

  • 结果 :.then() 返回的新 Promise (promise2) 将会变为 rejected (失败) 状态,并且你抛出的那个新错误,会成为 promise2 的失败原因。
typescript 复制代码
const initialPromise = Promise.reject(new Error("数据库连接失败"));

initialPromise
  .then(
    null, // onFulfilled: 忽略
    (error) => {
      // onRejected: 执行
      console.error("记录日志:", error.message);
      // 包装并抛出一个更具体的错误
      throw new Error("数据层异常,无法继续操作"); 
    }
  )
  .catch( // .catch 等价于 .then(null, onRejected)
    (nextError) => {
      // 这个 catch 会捕获到上面 throw 的新错误
      console.error("最终捕获:", nextError.message);
      // 输出: 最终捕获: 数据层异常,无法继续操作
    }
  );

发生了什么?

  • initialPromise 是 rejected 状态。
  • .then() 的 onRejected 被调用。
  • onRejected 内部 throw 了一个新错误。
  • .then() 方法本身返回了一个新的、失败的 Promise,其原因就是那个新错误。
  • 因此,后续的 .catch() 被调用。 这个机制就是"错误传递"或"错误转换"。它允许你在捕获一个底层错误后,将其包装成一个对上层业务更有意义的错误,再继续抛出。
场景 3:返回一个新的 Promise - "状态接管"

如果你在 onRejected 回调中返回了一个新的 Promise (我们称之为 promise3),那么 .then() 返回的 promise2 的状态将会完全依赖于 promise3 的最终状态。

  • 结果:promise2 会"接管" promise3 的命运。
  • 如果 promise3 最终 fulfilled,promise2 也会 fulfilled。 - 如果 promise3 最终 rejected,promise2 也会 rejected
javascript 复制代码
const initialPromise = Promise.reject(new Error("主API挂了"));

initialPromise
  .then(
    null, // onFulfilled: 忽略
    (error) => {
      // onRejected: 执行
      console.warn("主API失败,尝试调用备用API...", error.message);
      // 返回一个新的 Promise,尝试从备用源获取数据
      return fetchFromBackupAPI(); 
    }
  )
  .then(
    (backupResult) => {
      // 如果 fetchFromBackupAPI() 成功,这里会执行
      console.log("从备用API成功获取数据:", backupResult);
    }
  )
  .catch(
    (finalError) => {
      // 如果 fetchFromBackupAPI() 也失败了,这里会执行
      console.error("主API和备用API都失败了:", finalError);
    }
  );

// 辅助函数
function fetchFromBackupAPI() {
  return new Promise((resolve, reject) => {
    const success = true; // 模拟备用API成功或失败
    setTimeout(() => {
      if (success) {
        resolve({ data: "来自备用服务器的数据" });
      } else {
        reject(new Error("备用API也挂了"));
      }
    }, 500);
  });
}

这个机制非常强大,它允许你在一个异步操作失败后,启动另一个异步操作作为"B计划",并根据"B计划"的结果来决定整个流程的最终走向。

.then() 与 .catch() 的关系

.catch() 方法其实是 .then() 的一个"语法糖",它专门用来处理 rejected 状态。 (then方法中 onRejected函数的语法糖)

finally

finally 是一个用于指定 "无论 Promise 最终状态是成功(fulfilled)还是失败(rejected),都一定会执行" 的回调方法。它的核心作用是统一处理 "清理逻辑" (比如关闭连接、清除定时器、隐藏加载动画等),避免在 thencatch 中重复写相同的代码。

finally 的基础用法

finally 接收一个无参数的回调函数(因为它不关心 Promise 的结果是成功还是失败),且始终返回一个新的 Promise(便于继续链式调用)。

typescript 复制代码
Promise.resolve(/* 成功值 */)
  .then(res => { /* 处理成功逻辑 */ })
  .catch(err => { /* 处理失败逻辑 */ })
  .finally(() => { 
    /* 无论成功/失败,都会执行的清理逻辑 */ 
  });

Promise.resolve()

Promise.resolve() 的行为就像一个智能的包装器。它拿到一个 value 后,会先检查这个 value 的"身份",然后再决定如何包装。

场景 A:value 是一个普通值 (非 Promise, 非 thenable)

这是最简单的情况。

javascript 复制代码
const p = Promise.resolve("Hello");
// 等价于
const p = new Promise(resolve => resolve("Hello"));

它会创建一个新的、立即 fulfilled 的 Promise,并将 "Hello" 作为其结果。

场景 B:value 本身就是一个 Promise

Promise.resolve() 表现得非常"聪明"和"尊重"。

javascript 复制代码
const originalPromise = new Promise(resolve => setTimeout(() => resolve(42), 100));
const p = Promise.resolve(originalPromise);

console.log(p === originalPromise); // 输出: true
  • "该方法会直接返回这个 Promise" :这意味着 p 和 originalPromise 是同一个对象。Promise.resolve() 发现你给它的已经是一个 Promise 了,它就不会多此一举再去创建一个新的 Promise 包装它,而是直接把原来的那个返回给你。这是一种性能优化和行为一致性的体现。

Promise.reject()

与 resolve 的智能和包容不同,reject 的行为非常直接、简单、甚至有点"粗暴"

它的唯一使命就是:创建一个立即失败的 Promise,并且你给我的 reason 是什么,失败的原因就是什么,我绝不多做任何处理。

场景 A:reason 是一个普通值 (Error 对象, 字符串)
typescript 复制代码
const p = Promise.reject(new Error("Something went wrong"));
p.catch(err => {
  console.log(err.message); // 输出: "Something went wrong"
});

这是最常见的用法,非常直接。

场景 B:reason 是一个 Promise (关键区别所在)

现在,我们把一个 Promise 作为失败的原因传进去。

ini 复制代码
// 示例 1: reason 是一个 Promise
const reasonPromise = Promise.resolve("This is a successful promise!");
const p1 = Promise.reject(reasonPromise);

p1.catch(error => {
  console.log(error === reasonPromise); // 输出: true
  // 失败的原因,就是那个 `reasonPromise` 对象本身,而不是它解析后的值 "This is a successful promise!"
  error.then(val => console.log(val)); // 可以调用 .then,输出 "This is a successful promise!"
});

// 示例 2: reason 是一个 thenable
const reasonThenable = {
  then: function(resolve) { resolve("I am a thenable"); }
};
const p2 = Promise.reject(reasonThenable);

p2.catch(error => {
  console.log(error === reasonThenable); // 输出: true
  // 失败的原因,就是 `reasonThenable` 对象本身,它的 .then 方法根本不会被执行
});
  • 它也不会去'展开' :这意味着 Promise.reject() 完全不关心 你传给它的 reason 是什么类型。它不会去检查 reason 是不是 Promise,或者有没有 .then 方法。它直接把这个 reason 原封不动地当作最终的拒绝原因。

async await (像同步编程一样进行异步编程)

async - 声明一个"异步函数"
  • 作用 :async 关键字用于函数声明或函数表达式之前,表示这个函数是一个异步函数
  • 返回值 :一个 async 函数总是隐式地返回一个 Promise
    • 如果函数内部 return 了一个普通值(比如一个数字或字符串),那么这个值会被自动包装在一个 fulfilled (成功) 的 Promise 中。
    • 如果函数内部 throw 了一个错误 ,那么这个错误会被包装在一个 ****rejected (失败) 的 Promise 中。
    • 如果return了一个promise,那么async就返回这个 promise
javascript 复制代码
async function sayHello() {
  return "Hello, World!"; // 实际上返回的是 Promise.resolve("Hello, World!")
}

sayHello().then(console.log); // 输出: Hello, World!

async function throwError() {
  throw new Error("Something went wrong"); // 实际上返回的是 Promise.reject(new Error(...))
}

throwError().catch(console.error); // 输出: Error: Something went wrong
await - "暂停"并等待 Promise 结果
  • 作用await 关键字用于等待一个 Promise 对象的结果。它会"暂停" async 函数的执行,直到这个 Promise 的状态变为 fulfilled rejected
  • 使用限制 :await 只能在 async 函数内部使用。
  • 返回值
    • 如果 Promise 成功 (fulfilled),await 会"解包"这个 Promise,并返回其成功的结果值。
    • 如果 Promise 失败 (rejected),await 会将这个失败的原因(通常是一个 Error 对象)作为异常抛出

并行执行 :await 会让异步操作串行化。如果你有多个互不依赖的异步任务,不要一个接一个地 await,而应该使用 Promise.all() 来让它们并行执行,以提高效率。

Promise的用处

Promise让异步编程不再困难,promise在许多地方都有用,比如 axios,vue Router,所以了解promise的概念和用法很重要。

  1. Promise.all
  2. Promise.race
  3. Promise.any
  4. Promise.allSettled 等方法都指得
相关推荐
火柴就是我2 小时前
每日扫盲之TypeScript UMD 模块类型定义
前端
一点一木2 小时前
🚀 2025 年 09 月 GitHub 十大热门项目排行榜 🔥
前端·人工智能·github
lyj1689972 小时前
CSS中 min() max() clamp()函数
前端·javascript·css
年年测试2 小时前
Playwright web爬虫与AI智能体
前端·人工智能·爬虫
yi碗汤园2 小时前
【一文了解】C#的StringComparison枚举
开发语言·前端·c#
光影少年3 小时前
Promise.all实现其中有一个接口失败其他结果正常返回,如何实现?
前端·promise·掘金·金石计划
DokiDoki之父3 小时前
web核心—Tomcat的下载/配置/mavenweb项目创建/通过mavenweb插件运行web项目
java·前端·tomcat
我的div丢了肿么办3 小时前
echarts4升级为echarts5的常见问题
前端·javascript·echarts
ZoeLandia3 小时前
Vue 项目 JSON 在线编辑、校验如何选?
前端·vue.js·json