普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月18日首页

Promise基础知识整理,看看还有你不清楚的吗

作者 sophie旭
2026年1月17日 21:04

背景

Promise 作为 异步编程的老生常谈,这里不免俗也整理一番,以后关于 Promise基础知识看这篇就好了。整理过后,我想说一句话:回调函数可以说是javascript中,所有异步编程方式的根基,Promise 无非是 以更好维护更优雅的形式让我们使用回调函数,并不算是一个 全新的摆脱回调函数的解法。

Promise 构造函数

完整代码示例

// 1. 使用Promise构造函数创建一个新的Promise实例(承诺)
// 构造函数接收一个「兑现承诺的逻辑函数」,这个函数会被同步执行
const promise = new Promise((resolve, reject) => {
  console.log('Promise构造函数的执行函数:同步执行');
  
  // resolve和reject都是函数,用于修改Promise状态
  // 2. 调用resolve:将Promise状态改为fulfilled(成功),并传递结果
  resolve(100); // 这里传入固定值100作为异步任务的操作结果
  
  // 3. Promise状态一旦确定就无法修改,所以下面的reject不会生效(注释掉更直观)
  // reject(new Error('promise rejected')); // 将状态改为rejected(失败),传递错误理由
});

// 4. 用then方法指定状态变更后的回调
// then接收两个参数:onFulfilled(成功回调)、onRejected(失败回调)
promise.then(
  // onFulfilled:Promise状态为fulfilled时执行,接收resolve传递的结果
  (value) => {
    console.log('Promise成功:', value); // 输出 Promise成功: 100
  },
  // onRejected:Promise状态为rejected时执行,接收reject传递的错误
  (error) => {
    console.log('Promise失败:', error.message);
  }
);

console.log('同步代码:在Promise创建后执行');

代码输出结果

Promise构造函数的执行函数:同步执行
同步代码:在Promise创建后执行
Promise成功: 100

逐点解读(核心解释)

  1. 构造函数参数(兑现承诺的逻辑)

    • new Promise((resolve, reject) => { ... }) 中的箭头函数就是「兑现承诺的逻辑」。
    • 这个函数同步执行:所以先输出 Promise构造函数的执行函数:同步执行,再执行后续的同步代码。
  2. resolve 和 reject 参数

    • 二者都是浏览器内置的函数,不是我们定义的。
    • resolve(100):把 Promise 状态改为 fulfilled(成功),并把 100 作为「成功结果」传递给 then 的第一个回调。
    • reject(new Error('promise rejected')):把状态改为 rejected(失败),并把错误对象作为「失败理由」传递给 then 的第二个回调。
  3. 状态一旦确定就不能修改

    • 代码中先调用了 resolve(100),此时 Promise 状态已经固定为成功,即便后续调用 reject(哪怕取消注释),也不会改变状态,then 的失败回调永远不会执行。
  4. then 方法的回调

    • then 的第一个参数:只有 Promise 状态为 fulfilled 时才执行,接收 resolve 传递的值。
    • then 的第二个参数:只有 Promise 状态为 rejected 时才执行,接收 reject 传递的错误。
    • 注意:then 的回调是微任务,所以会等所有同步代码执行完后才执行(先输出 同步代码:在Promise创建后执行,再输出 Promise成功: 100)。

总结

  1. Promise 构造函数的执行函数是同步执行的,里面的 resolve/reject 用于修改 Promise 状态(成功/失败)。
  2. Promise 状态一旦通过 resolvereject 确定,就永久不可修改,后续调用另一个函数也无效。
  3. then 方法的两个回调分别对应「成功状态」和「失败状态」的处理逻辑,且回调是微任务(晚于同步代码执行)。

Promise 链式调用 用起来!

错误写法:嵌套then(回调地狱)

使用Promise「常见误区」,本质和传统回调嵌套没区别,完全浪费了Promise的优势:

// 错误:嵌套使用then,形成回调地狱
ajax('urls.json').then(
  (res) => {
    console.log('第一步:拿到urls.json结果', res);
    // 误区:在第一个then的回调里嵌套第二个then
    ajax(res.userUrl).then(
      (userRes) => {
        console.log('第二步:拿到用户数据', userRes);
        // 如果还有第三个请求,会继续嵌套,代码越来越深
      },
      (err) => {
        console.log('第二步请求失败', err);
      }
    );
  },
  (err) => {
    console.log('第一步请求失败', err);
  }
);

这种写法的问题:

  • 代码层级嵌套,越往后越深,可读性差(回调地狱)
  • 错误处理需要在每个嵌套的then里单独写,冗余且麻烦

正确写法:then链式调用(扁平化)

核心原理:then 方法会返回一个新的 Promise,所以可以直接链式调用,而非嵌套。

// 正确:链式调用then,扁平化代码
ajax('urls.json')
  .then((res) => {
    console.log('第一步:拿到urls.json结果', res);
    // 关键:返回下一个异步任务的Promise
    return ajax(res.userUrl); 
  })
  .then((userRes) => {
    console.log('第二步:拿到用户数据', userRes);
    // 可以继续链式调用第三个异步任务
    // return ajax(第三个地址);
  })
  .catch((err) => {
    // 统一错误处理:任何一步失败都会走到这里
    console.log('请求失败:', err.message);
  });
执行结果(500ms后)
第一步:拿到urls.json结果 { userUrl: '/user' }
第二步:拿到用户数据 { name: '张三', age: 20 }

核心逻辑拆解(为什么链式调用能避免回调地狱)

  1. ajax('urls.json').then(...) 执行后,返回一个新的 Promise(记为 P1)。
  2. 第一个 then 的回调里 return ajax(res.userUrl),这个 ajax 调用会返回另一个 Promise(记为 P2)。
  3. Promise 的规则:如果 then 的回调返回一个 Promise(P2),那么 then 对应的新 Promise(P1)会「继承」P2 的状态——P2 成功,P1 就成功;P2 失败,P1 就失败。
  4. 第二个 then(...) 其实是挂载在 P1 上的回调,而非嵌套在第一个 then 内部,所以代码是扁平的。

总结

  1. Promise 避免回调地狱的核心是 then 的链式调用,而非嵌套使用 then。
  2. 关键规则:then 会返回新 Promise,若 then 回调返回 Promise,则新 Promise 继承该 Promise 的状态。
  3. 链式调用+catch 可以实现扁平化代码结构统一错误处理,这是 Promise 对比传统回调的核心优势。

Promise 链式调用 的 特殊性

先对比两种链式调用的本质区别

1. 传统链式调用(返回 this)

比如 jQuery 的链式调用,核心是方法内部返回 this(自身),所有方法都操作同一个对象:

// 传统链式调用:返回this
class MyObj {
  name = '';
  setName(name) {
    this.name = name;
    return this; // 返回自身(同一个对象)
  }
  logName() {
    console.log(this.name);
    return this; // 返回自身
  }
}

const obj = new MyObj();
obj.setName('张三').logName(); 
// 这里 setName 和 logName 操作的是同一个 obj 对象
console.log(obj.setName('张三') === obj); // true(返回的是同一个对象)
2. Promise 的链式调用(返回新对象)

Promise 的 then 每调用一次,都会生成一个全新的 Promise,和原对象毫无关系:

// Promise链式调用:返回新对象
const p1 = new Promise((resolve) => resolve(100));
const p2 = p1.then((res) => res + 10); // p2 是全新的Promise
const p3 = p2.then((res) => res + 10); // p3 是全新的Promise

console.log(p1 === p2); // false(不是同一个对象)
console.log(p2 === p3); // false(不是同一个对象)

// 执行结果:验证每个then对应不同的Promise
p3.then((res) => console.log(res)); // 120

逐句拆那段「绕口的话」

如果我们这里不断的链式调用then方法,然后呢这里每一个then方法,它实际上都是在为上一个then方法返回的promise对象去添加状态明确过后的回调。

我用「分步拆解+代码标注」的方式解释:

// 第一步:创建原始Promise p1(第一个承诺)
const p1 = new Promise((resolve) => resolve('初始值'));

// 第二步:调用p1.then() → 返回新Promise p2(第二个承诺)
// 这个then是给p1加回调:p1成功后执行回调,然后决定p2的状态
const p2 = p1.then((res) => {
  console.log('p1的回调:', res); // 输出:p1的回调: 初始值
  return 'p1回调的返回值'; // 这个返回值会决定p2的状态(成功,值为这个字符串)
});

// 第三步:调用p2.then() → 返回新Promise p3(第三个承诺)
// 这个then是给p2加回调:p2成功后执行回调,然后决定p3的状态
const p3 = p2.then((res) => {
  console.log('p2的回调:', res); // 输出:p2的回调: p1回调的返回值
  return 'p2回调的返回值';
});

// 第四步:调用p3.then() → 返回新Promise p4(第四个承诺)
// 这个then是给p3加回调:p3成功后执行回调,然后决定p4的状态
const p4 = p3.then((res) => {
  console.log('p3的回调:', res); // 输出:p3的回调: p2回调的返回值
});
拆解逻辑(对应你那段话):
  1. 「不断链式调用 then」→ 代码中 p1.then() → p2.then() → p3.then() 就是链式调用。
  2. 「每一个 then 方法,都是为上一个 then 返回的 Promise 对象加回调」:
    • p2.then(...) → 是给「p1.then() 返回的 p2」加回调;
    • p3.then(...) → 是给「p2.then() 返回的 p3」加回调;
    • 每个 then 都不是给原始的 p1 加回调,而是给「上一个 then 生成的新 Promise」加回调。
  3. 「状态明确过后的回调」:只有当被绑定的 Promise(比如 p2)状态变为 fulfilled/rejected,这个 then 的回调才会执行。

为什么要返回全新的 Promise?(核心目的)

返回全新Promise的目的是实现Promise链条,一个承诺结束后返回新承诺,每个承诺负责一个异步任务,相互无影响」,用例子验证:

// 场景:第一个异步任务(延迟1s),第二个异步任务(延迟2s)
const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('第一个异步任务完成'), 1000);
});

// 第一个then:负责第一个异步任务的结果处理,返回新Promise(第二个异步任务)
const p2 = p1.then((res) => {
  console.log(res); // 1s后输出:第一个异步任务完成
  // 返回新Promise(第二个异步任务),和p1完全独立
  return new Promise((resolve) => {
    setTimeout(() => resolve('第二个异步任务完成'), 2000);
  });
});

// 第二个then:只关心p2(第二个异步任务)的状态,和p1无关
p2.then((res) => {
  console.log(res); // 再等2s后输出:第二个异步任务完成
});

// 此时操作p1,不会影响p2
p1.then(() => console.log('p1的另一个回调')); // 1s后输出,和p2无关
  • 每个 Promise(p1/p2)都是独立的,p1 完成不影响 p2 的执行逻辑,p2 延迟也不会干扰 p1 的其他回调;
  • 若 then 返回 this(同一个对象),则无法实现「一个异步任务完成后,再启动下一个独立的异步任务」,因为所有 then 都绑定在同一个对象上,状态只能变一次。

总结

  1. Promise 链式调用≠传统链式调用:传统是返回 this(同一对象),Promise 是返回全新的 Promise 对象
  2. 链式调用的本质:每个 then 都是给「上一个 then 返回的新 Promise」绑定回调,而非给原始 Promise 绑定。
  3. 返回新 Promise 的核心价值:让每个异步任务都对应一个独立的「承诺」,任务之间相互独立、按顺序执行,实现真正的异步链条。
补充:then 回调返回 Promise → 后一个 then 等待该 Promise 结束
  • 第二个 then 回调返回 delayTask(1000, ...)(一个需要等待1s的 Promise);
  • 此时第三个 then 不会立即执行,而是等待这个返回的 Promise 状态变为 fulfilled(1s后完成);
  • 等价于:第三个 then 直接绑定到「第二个 then 返回的这个 delayTask Promise」上,成为它的回调。

为了让你更清楚「后一个 then 等价于给返回的 Promise 注册回调」,把上面的代码拆成非链式写法,逻辑完全一致:

// 拆分成非链式写法,等价于上面的链式调用
const p1 = delayTask(1000, '第一个异步任务结果');

// 第一个then:返回p2
const p2 = p1.then((res1) => {
  console.log('第一个then回调执行:', res1);
  return '第一个then的返回值(普通值)';
});

// 第二个then:返回p3
const p3 = p2.then((res2) => {
  console.log('第二个then回调执行:', res2);
  // 返回一个新的Promise p_temp
  const p_temp = delayTask(1000, '第二个then返回的Promise结果');
  return p_temp;
});

// 第三个then:等价于给p_temp注册回调(因为p3的状态由p_temp决定)
const p4 = p3.then((res3) => {
  console.log('第三个then回调执行:', res3);
  return '最终结果';
});

// 等价于直接给p_temp注册回调:
// p_temp.then((res3) => {
//   console.log('第三个then回调执行:', res3);
// });

catch() 与 then(成功, 失败) 的失败回调 是否完全等价?

先明确核心结论(先记重点)

  • catch() 等价于 then(undefined, 失败回调),但绑定的是上一个 then 返回的新 Promise
  • then(成功回调, 失败回调) 中的失败回调,只绑定当前 Promise,管不到后续 then 里的新 Promise 异常;
  • Promise 链条中,异常会「向后传递」,直到被某个失败回调捕获。

对比示例:then第二个参数 vs catch(直观看差异)

我们用「两步异步任务」的场景,模拟第一步成功、第二步失败的情况,对比两种写法的结果:

第一步:封装模拟异步函数
// 模拟异步任务1:一定成功,返回"第一步结果"
function task1() {
  return new Promise((resolve) => {
    resolve("第一步结果");
  });
}

// 模拟异步任务2:一定失败,抛出异常
function task2() {
  return new Promise((resolve, reject) => {
    reject(new Error("第二步执行失败"));
  });
}
场景1:用 then 的第二个参数注册失败回调(只能捕获第一步异常)
// 写法1:then(成功回调, 失败回调)
task1()
  .then(
    // 成功回调:第一步成功后执行,调用task2(返回失败的Promise)
    (res) => {
      console.log("第一步成功:", res);
      return task2(); // 返回一个失败的新Promise(记为P2)
    },
    // 失败回调:只绑定task1返回的Promise(记为P1),只能捕获P1的异常
    (err) => {
      console.log("捕获到异常:", err.message);
    }
  );

/* 输出结果:
第一步成功: 第一步结果
Uncaught (in promise) Error: 第二步执行失败
*/

关键问题:第二步的异常没被捕获!因为 then 的第二个参数只负责「task1 返回的 P1」,管不到「第一个 then 返回的 P2(task2 的 Promise)」的异常。

场景2:用 catch 注册失败回调(能捕获整个链条的异常)
// 写法2:then(成功回调) + catch(失败回调)
task1()
  .then((res) => {
    console.log("第一步成功:", res);
    return task2(); // 返回失败的P2
  })
  .catch((err) => {
    // catch绑定的是「上一个then返回的P2」,能捕获P2的异常
    console.log("捕获到异常:", err.message);
  });

/* 输出结果:
第一步成功: 第一步结果
捕获到异常: 第二步执行失败
*/

核心原因:catch 等价于 then(undefined, 失败回调),这个失败回调绑定在「第一个 then 返回的 P2」上,刚好能捕获 P2 的异常。

拆解异常传递+回调绑定逻辑(为什么会这样?)

我们用「Promise 链条对象关系」来拆解上面的代码:

task1() → 返回 P1(成功状态)
↓
P1.then(成功回调, 失败回调) → 返回 P2(由成功回调的返回值决定:task2() 返回失败的Promise → P2 失败)
↓
P2.catch(失败回调) → 绑定在 P2 上,捕获 P2 的失败
关键细节:
  1. then(成功回调, 失败回调) 的失败回调 → 只绑定 P1,只能处理 P1 的异常(比如 task1 失败);
  2. catch() → 绑定 P2,能处理 P2 的异常(包括 P2 自身失败、或 P1 未被捕获的异常向后传递过来);
  3. 异常传递规则:如果一个 Promise 失败且没有对应的失败回调,异常会「顺着链条往后传」,直到被某个 catch/then 失败回调捕获。

补充:如果第一步就失败,两种写法的表现

// 改造task1:让第一步直接失败
function task1() {
  return new Promise((resolve, reject) => {
    reject(new Error("第一步执行失败"));
  });
}

// 写法1:then的第二个参数 → 能捕获P1的异常
task1().then(
  (res) => console.log(res),
  (err) => console.log("then捕获:", err.message) // 输出:then捕获:第一步执行失败
);

// 写法2:catch → 也能捕获(因为P1的异常传递到P2,被catch捕获)
task1()
  .then((res) => console.log(res))
  .catch((err) => console.log("catch捕获:", err.message)); // 输出:catch捕获:第一步执行失败

这说明:catch 能捕获整个链条中「前面所有未被处理的异常」,而 then 第二个参数只能捕获「当前 Promise」的异常

为什么 catch 更适合链式调用?

  • 链式调用的核心是「多个异步任务依次执行」,每个任务对应链条中的一个 Promise;
  • 用 catch 可以「统一捕获整个链条的所有异常」,无需在每个 then 里写失败回调;
  • 用 then 第二个参数则需要「每个 then 都写失败回调」,否则后续 Promise 的异常会逃逸(未捕获)。

总结

  1. catch()then(undefined, 失败回调) 的语法糖,但绑定的是上一个 then 返回的新 Promise,而非原始 Promise;
  2. then(成功, 失败) 的失败回调仅绑定当前 Promise,无法捕获后续 then 中返回的新 Promise 异常;
  3. Promise 异常会「向后传递」,catch 因绑定在链条末端的 Promise 上,能捕获整个链条的所有未处理异常,这也是它更适合链式调用的核心原因。

unhandledrejection 是否推荐使用

一、先简单了解全局捕获(仅作认知,不推荐使用)

1. 浏览器环境(window 上注册 unhandledrejection)
// 浏览器中全局捕获未处理的Promise异常(仅演示,不推荐)
window.addEventListener('unhandledrejection', (event) => {
  // 阻止浏览器默认的错误提示(比如控制台的红色报错)
  event.preventDefault();
  console.log('全局捕获未处理的Promise异常:', event.reason.message);
});

// 测试:抛出一个未手动捕获的Promise异常
new Promise((resolve, reject) => {
  reject(new Error('这是一个未被手动捕获的异常'));
});
// 控制台会输出:全局捕获未处理的Promise异常:这是一个未被手动捕获的异常
2. Node.js 环境(process 上注册 unhandledRejection)
// Node.js中全局捕获(仅演示,不推荐)
process.on('unhandledRejection', (reason, promise) => {
  console.log('全局捕获未处理的Promise异常:', reason.message);
});

// 测试
new Promise((resolve, reject) => {
  reject(new Error('Node中未被手动捕获的异常'));
});

二、为什么强烈不推荐全局捕获?(核心原因)

「不推荐全局统一处理」,核心问题有这几点:

  1. 调试困难:全局捕获会「兜底」所有未处理的异常,但无法精准定位异常发生的位置——一个大型项目中,你无法从全局回调里快速知道是哪一行代码、哪个异步任务抛出的异常。
  2. 掩盖问题:全局捕获会让开发者产生「反正有兜底,不用手动写 catch」的惰性,导致代码中大量异常没有被「针对性处理」(比如某个接口失败需要重试,另一个需要提示用户,全局捕获只能统一打印,无法差异化处理)。
  3. 不可控性:全局事件是「最后一道防线」,若代码中漏写 catch,全局捕获会接住异常,但这属于「被动补救」,而非「主动处理」,容易埋下线上bug(比如异常处理逻辑不匹配场景)。

三、更优的做法:显式捕获每一个可能的异常

最佳实践是「链式调用末尾加 catch」+「针对不同场景差异化处理异常」,甚至可以给不同异步任务加「专属的异常处理」。

示例1:基础版——链式末尾统一 catch
// 模拟两个异步任务,第二步可能失败
function task1() {
  return new Promise((resolve) => resolve('第一步成功'));
}

function task2() {
  return new Promise((resolve, reject) => {
    // 模拟随机失败
    Math.random() > 0.5 
      ? resolve('第二步成功') 
      : reject(new Error('第二步接口调用失败'));
  });
}

// 显式捕获:链式末尾加catch,针对性处理
task1()
  .then((res) => {
    console.log(res);
    return task2();
  })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    // 精准处理:区分不同异常,做不同操作
    if (err.message === '第二步接口调用失败') {
      console.log('处理第二步失败:', '重试一次或提示用户');
    } else {
      console.log('其他异常:', err.message);
    }
  });
示例2:进阶版——分阶段捕获(不同任务单独处理)

如果某个异步任务的异常需要「单独处理,不中断后续流程」,可以在该任务的 then 后紧跟 catch:

task1()
  .then((res) => {
    console.log(res);
    // 第二步失败后单独处理,不影响后续流程
    return task2().catch((err) => {
      console.log('第二步单独处理失败:', err.message);
      return '第二步失败后的兜底值'; // 返回兜底值,让链条继续
    });
  })
  .then((res) => {
    // `无论第二步成功/失败,都会执行这里`--- 重点!!!
    console.log('第三步:接收第二步结果', res);
  })
  .catch((err) => {
    // 捕获其他未处理的异常
    console.log('全局兜底(极少触发):', err.message);
  });

总结

  1. 全局 unhandledrejection/unhandledRejection 是「兜底方案」,仅适合临时调试或紧急补救,不推荐作为常规异常处理方式
  2. 最佳实践是「显式捕获」:在 Promise 链条末尾加 catch,针对不同异常做「差异化处理」(重试、兜底、提示用户等);
  3. 若需要保留链条执行,可在单个异步任务后紧跟 catch,返回兜底值,避免整个链条中断。
  4. 关于catch返回值:
    • 如果 catch 回调返回正常值(普通值 / 成功的 Promise)→ 新 Promise 状态为 fulfilled(成功);
    • 如果 catch 回调抛出异常 / 返回失败的 Promise → 新 Promise 状态为 rejected(失败)
  • 如果 catch 回调抛出异常 / 返回失败的 Promise → 新 Promise 状态为 rejected(失败)

Promise.reslove

一、Promise.resolve() 基本用法

Promise.resolve(value) 是创建「已成功 Promise」的快捷方式,无需手动写 new Promise + resolve,核心逻辑就是:接收一个值,返回一个状态为 fulfilled 的 Promise,且该值会作为 Promise 的成功结果

代码示例:Promise.resolve() 基础使用
// 用 Promise.resolve 快速创建成功的 Promise
const p1 = Promise.resolve('foo');

// 调用 then 接收结果(你提到的 unfulfilled 是笔误,正确是 fulfilled)
p1.then((res) => {
  console.log('成功回调拿到的值:', res); // 输出:成功回调拿到的值:foo
});

二、等价逻辑:Promise.resolve() ≈ new Promise + resolve

你提到「这种方式完全等价于 new Promise 然后直接 resolve 该值」,我们用代码验证这个等价性:

// 方式1:Promise.resolve 快捷写法
const p1 = Promise.resolve('foo');

// 方式2:new Promise 完整写法(和方式1完全等价)
const p2 = new Promise((resolve) => {
  // 在执行函数中直接 resolve 'foo',Promise 状态立即变为 fulfilled
  resolve('foo');
});

// 测试两个 Promise 的执行结果(完全一致)
p1.then(res => console.log('p1结果:', res)); // p1结果:foo
p2.then(res => console.log('p2结果:', res)); // p2结果:foo
核心等价点:
  • 两者创建的 Promise 状态都是 fulfilled(成功);
  • 两者的成功回调拿到的参数都是传入的 'foo'
  • 两者的执行时机一致:Promise.resolve() 内部的逻辑和 new Promise 的执行函数一样,是同步执行的(但回调仍为微任务)。

三、Promise.resolve() 的进阶场景(拓展理解)

除了传入普通值(字符串、数字等),Promise.resolve() 还有两个常见场景,帮你全面掌握:

场景1:传入 Promise 对象

如果传入的是一个已存在的 Promise,Promise.resolve() 会直接返回这个 Promise(不会创建新对象):

const originalPromise = new Promise((resolve) => resolve('原始Promise'));
const wrappedPromise = Promise.resolve(originalPromise);

console.log(originalPromise === wrappedPromise); // true(返回同一个对象)
wrappedPromise.then(res => console.log(res)); // 原始Promise
场景2:传入「类 Promise 对象」(thenable)

如果传入的是有 then 方法的对象(称为 thenable),Promise.resolve() 会执行其 then 方法,将其转换成标准 Promise:

// 定义一个 thenable 对象(有 then 方法,但不是真正的 Promise)
const thenable = {
  then(resolve) {
    resolve('thenable 转换的结果');
  }
};

// Promise.resolve 会执行 then 方法,转换成标准 Promise
Promise.resolve(thenable).then(res => {
  console.log(res); // 输出:thenable 转换的结果
});

四、为什么要用 Promise.resolve()?

相比 new Promise 写法,Promise.resolve() 的优势在于:

  1. 简化代码:创建已成功的 Promise 时,少写嵌套的执行函数,代码更简洁;
  2. 统一接口:当你不确定一个值是普通值还是 Promise 时,用 Promise.resolve() 可以「归一化」成 Promise,方便链式调用:
    // 假设 fn 可能返回普通值,也可能返回 Promise
    function fn() {
      return Math.random() > 0.5 ? '普通值' : Promise.resolve('Promise值');
    }
    
    // 用 Promise.resolve 统一处理,无需区分类型
    Promise.resolve(fn()).then(res => {
      console.log('统一拿到结果:', res);
    });
    

总结

  1. Promise.resolve(value) 是创建状态为 fulfilled 的 Promise 的快捷方式,等价于 new Promise((resolve) => resolve(value))
  2. 传入普通值时,该值会作为 Promise 的成功结果,在 then 的成功回调中获取;
  3. 传入 Promise/thenable 对象时,Promise.resolve() 会适配并返回标准 Promise,核心作用是「归一化」值的类型,方便异步处理。

核心记住:Promise.resolve() 的本质是「快速生成成功的 Promise」,减少冗余代码,统一异步/同步值的处理逻辑。

Promise.reject

Promise.reject() 快速创建失败的 Promise

Promise.reject() 是创建「状态为 rejected(失败)」Promise 的快捷方式,你提到「无论传入什么参数,都会作为失败理由」,代码验证:

// 1. 传入普通值(字符串)
const p1 = Promise.reject('普通错误信息');
p1.catch(err => console.log('p1失败理由:', err)); // 输出:p1失败理由:普通错误信息

// 2. 传入 Error 对象(推荐写法,包含堆栈信息)
const p2 = Promise.reject(new Error('标准错误对象'));
p2.catch(err => console.log('p2失败理由:', err.message)); // 输出:p2失败理由:标准错误对象

// 3. 传入 Promise 对象(和 resolve 不同,不会原样返回,而是直接作为失败理由)
const originalPromise = Promise.resolve('成功的Promise');
const p3 = Promise.reject(originalPromise);
p3.catch(err => {
  console.log('p3失败理由是原Promise:', err === originalPromise); // 输出:true
});
关键区别(和 Promise.resolve 对比):
  • Promise.resolve(已存在的Promise) → 返回原 Promise;
  • Promise.reject(已存在的Promise) → 不会返回原 Promise,而是把这个 Promise 对象直接作为失败理由

总结

  1. Promise.resolve(x) 规则:
    • x 是普通值 → 返回 fulfilled 状态的 Promise,x 为成功结果;
    • x 是 Promise → 原样返回 x;
    • x 是 thenable 对象 → 转换成原生 Promise,执行其 then 方法。
  2. Promise.reject(reason) 规则:
    • 无论 reason 是普通值、Error 对象、甚至 Promise 对象,都会直接作为「失败理由」,返回 rejected 状态的 Promise;
    • 推荐传入 Error 对象(而非字符串),便于调试(包含错误堆栈)。
  3. Promise.resolve/reject 的核心价值:简化 Promise 创建代码,统一异步值的处理逻辑(尤其是 resolve 对 thenable 的兼容)。

为什么 Promise 的递归调用会导致浏览器卡死,而 setTimeout 的递归调用通常不会?

先看直观对比(代码+现象)

先跑两段代码,直观感受差异:

示例1:Promise 递归(卡死浏览器)
// Promise 递归:同步占用主线程,无喘息机会
function promiseRecursion() {
  Promise.resolve().then(() => {
    console.log("Promise 递归执行");
    promiseRecursion(); // 递归调用
  });
}
promiseRecursion();

现象:浏览器标签页卡顿、无响应,控制台疯狂输出,但页面无法交互,甚至会触发「页面无响应」提示。

示例2:setTimeout 递归(不卡死)
// setTimeout 递归:每次执行后释放主线程
function timeoutRecursion() {
  setTimeout(() => {
    console.log("setTimeout 递归执行");
    timeoutRecursion(); // 递归调用
  }, 0);
}
timeoutRecursion();

现象:控制台持续输出,但页面仍能点击、滚动,浏览器完全不卡顿。

核心原因拆解(事件循环+调用栈)

浏览器的主线程是「单线程」,所有 JS 执行、DOM 渲染、事件响应都在这一个线程里,能否「释放主线程」是是否卡死的关键:

1. Promise 递归:微任务「抢占式」执行,调用栈永不清空
  • Promise.then 的回调属于「微任务」:微任务的执行规则是「当前宏任务执行完毕后,立即清空所有微任务队列,再执行下一个宏任务/渲染/事件」。
  • 递归逻辑
    1. 第一次调用 promiseRecursion()Promise.resolve() 生成微任务 A;
    2. 当前宏任务执行完,执行微任务 A → 打印日志,调用 promiseRecursion() → 生成微任务 B;
    3. 微任务 A 执行完,立即执行微任务 B → 打印日志,生成微任务 C;
    4. 这个过程无限循环,微任务队列永远有新任务,主线程被微任务「占满」,没有任何时间片分配给:
      • DOM 渲染(页面卡死);
      • 鼠标点击/滚动等事件响应(交互失效);
      • 其他宏任务(比如 setTimeout、网络请求)。
  • 调用栈角度:虽然每次 then 回调执行完会清空当前调用栈,但微任务的「连续执行」让主线程没有「空闲期」,本质是「无限的同步执行流」。
2. setTimeout 递归:宏任务「排队式」执行,每次释放主线程
  • setTimeout 的回调属于「宏任务」:宏任务的执行规则是「执行完一个宏任务后,先执行所有微任务,再处理渲染,再取下一个宏任务」。
  • 递归逻辑
    1. 第一次调用 timeoutRecursion()setTimeout 把回调 A 加入「宏任务队列」;
    2. 当前宏任务执行完,执行微任务 → 渲染页面 → 处理事件(点击/滚动)→ 再执行宏任务 A;
    3. 宏任务 A 执行:打印日志,调用 timeoutRecursion() → 把回调 B 加入宏任务队列;
    4. 宏任务 A 执行完,主线程会「释放」,先处理渲染、事件响应,再执行下一个宏任务 B;
    5. 这个过程虽然无限,但每次宏任务执行完都会给主线程喘息机会,页面渲染、事件响应能正常进行,因此不会卡死。

补充:为什么 Promise 微任务要「立即执行」?

微任务的设计初衷是「处理异步但需要尽快完成的逻辑」(比如 Promise 回调、async/await),优先级高于宏任务和渲染,这保证了异步逻辑的执行顺序,但无限递归的微任务会滥用这个优先级,导致主线程阻塞。

总结

  1. 核心差异:Promise 递归是「微任务无限连续执行」,主线程无喘息机会;setTimeout 递归是「宏任务排队执行」,每次执行后释放主线程,允许渲染/事件响应。
  2. 调用栈/队列:Promise 递归让微任务队列永远非空,主线程被占满;setTimeout 递归的宏任务队列虽有任务,但每次执行完会处理渲染和事件。
  3. 本质:浏览器卡死的核心是「主线程无法处理渲染/交互」,而非「递归本身」——setTimeout 递归给了主线程处理这些的时间,而 Promise 递归没有。

如果想让 Promise 递归不卡死,可在递归中加入 setTimeout 「让出主线程」:

// 改进版 Promise 递归:不卡死
function promiseRecursion() {
  Promise.resolve().then(() => {
    console.log("Promise 递归执行");
    setTimeout(() => promiseRecursion(), 0); // 用setTimeout让出主线程
  });
}
promiseRecursion();

Promise 的 then 方法的核心实现

先明确 then 方法的核心需求

  1. then 接收两个参数:onFulfilled(成功回调)、onRejected(失败回调);
  2. 回调需异步执行(微任务,这里用 setTimeout 模拟);
  3. 若 Promise 状态未确定(pending),需先存储回调;若已确定,直接执行回调;
  4. then 必须返回新的 Promise,实现链式调用;
  5. 上一个 then 的回调返回值,决定新 Promise 的状态。

极简版 Promise + then 实现

// 模拟 Promise 的核心实现(仅保留 then 方法的核心逻辑)
class MyPromise {
  // 定义三种状态
  static PENDING = 'pending';
  static FULFILLED = 'fulfilled';
  static REJECTED = 'rejected';

  constructor(executor) {
    // 初始状态
    this.status = MyPromise.PENDING;
    // 成功结果
    this.value = undefined;
    // 失败原因
    this.reason = undefined;
    // 存储 pending 状态时的回调(因为此时状态未确定,需等待)
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // resolve 函数:修改状态为成功,执行存储的成功回调
    const resolve = (value) => {
      // 状态不可逆:只有 pending 时才能修改
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        // 执行所有存储的成功回调
        this.onFulfilledCallbacks.forEach(callback => callback());
      }
    };

    // reject 函数:修改状态为失败,执行存储的失败回调
    const reject = (reason) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.REJECTED;
        this.reason = reason;
        // 执行所有存储的失败回调
        this.onRejectedCallbacks.forEach(callback => callback());
      }
    };

    // 执行器函数同步执行,捕获执行过程中的异常
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  // 核心:实现 then 方法
  then(onFulfilled, onRejected) {
    // 兼容:如果没传回调,透传结果(比如 then().then() 的场景)
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

    // 关键:then 返回新的 Promise,实现链式调用
    const newPromise = new MyPromise((resolve, reject) => {
      // 封装回调执行逻辑(复用代码)
      const executeCallback = (callback, data) => {
        // 异步执行回调(用 setTimeout 模拟微任务)
        setTimeout(() => {
          try {
            // 执行回调,获取返回值
            const result = callback(data);
            // 核心规则:回调返回值决定新 Promise 的状态
            resolvePromise(newPromise, result, resolve, reject);
          } catch (error) {
            // 回调执行出错,新 Promise 状态为失败
            reject(error);
          }
        }, 0);
      };

      // 1. 如果当前 Promise 已成功
      if (this.status === MyPromise.FULFILLED) {
        executeCallback(onFulfilled, this.value);
      }

      // 2. 如果当前 Promise 已失败
      if (this.status === MyPromise.REJECTED) {
        executeCallback(onRejected, this.reason);
      }

      // 3. 如果当前 Promise 还是 pending(状态未确定),存储回调
      if (this.status === MyPromise.PENDING) {
        this.onFulfilledCallbacks.push(() => {
          executeCallback(onFulfilled, this.value);
        });
        this.onRejectedCallbacks.push(() => {
          executeCallback(onRejected, this.reason);
        });
      }
    });

    return newPromise;
  }
}

// 辅助函数:处理 then 回调的返回值,决定新 Promise 的状态
function resolvePromise(newPromise, result, resolve, reject) {
  // 避免循环引用(比如回调返回 newPromise 本身)
  if (result === newPromise) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // 1. 如果返回值是 Promise 实例
  if (result instanceof MyPromise) {
    // 等待该 Promise 完成,再决定新 Promise 的状态
    result.then(resolve, reject);
  } else {
    // 2. 如果返回值是普通值,直接 resolve 新 Promise
    resolve(result);
  }
}

核心逻辑拆解(重点理解)

1. 状态管理
  • 初始状态为 pending,只有调用 resolve/reject 且状态为 pending 时,才能修改状态;
  • 状态不可逆,一旦变为 fulfilled/rejected,无法再改。
2. 回调存储(pending 状态)
  • 如果调用 then 时,Promise 还处于 pending(比如异步任务没完成),会把回调存储到数组中;
  • 等状态确定后(调用 resolve/reject),遍历执行存储的回调。
3. 异步执行回调
  • setTimeout 模拟微任务(真实 Promise 是微任务,优先级比宏任务高,这里简化);
  • 确保回调不会同步执行,符合 Promise 规范。
4. 链式调用的核心(返回新 Promise)
  • then 必须返回新的 MyPromise,而非 this
  • 回调的返回值通过 resolvePromise 处理:
    • 返回普通值 → 新 Promise 状态为 fulfilled
    • 返回 Promise 实例 → 等待该实例完成,继承其状态;
    • 回调抛出异常 → 新 Promise 状态为 rejected

测试代码(验证 then 功能)

// 测试1:基础使用
const p1 = new MyPromise((resolve) => {
  setTimeout(() => resolve(100), 1000);
});

p1.then(res => {
  console.log('第一次then:', res); // 1s后输出:第一次then:100
  return res + 10; // 返回普通值
}).then(res => {
  console.log('第二次then:', res); // 输出:第二次then:110
  return new MyPromise(resolve => resolve(res + 10)); // 返回Promise
}).then(res => {
  console.log('第三次then:', res); // 输出:第三次then:120
});

// 测试2:失败场景
const p2 = new MyPromise((_, reject) => {
  reject(new Error('失败了'));
});

p2.then(
  res => console.log(res),
  err => {
    console.log('失败回调:', err.message); // 输出:失败回调:失败了
    throw new Error('回调里抛错');
  }
).catch(err => { // 注:catch 本质是 then(undefined, onRejected),可自行补充实现
  console.log('捕获回调错误:', err.message); // 输出:捕获回调错误:回调里抛错
});

补充:catch 方法(可选)

如果想补充 catch 方法,只需在 MyPromise 中加一行:

catch(onRejected) {
  return this.then(undefined, onRejected);
}

总结

  1. then 方法的核心是「状态判断 + 回调存储/执行 + 返回新 Promise」;
  2. 异步执行回调、状态不可逆、链式调用(返回新 Promise)是 then 的三大关键特性;
  3. 这个极简实现去掉了复杂的边界处理(如 thenable 对象、多次调用 then 等),但保留了 then 最核心的逻辑,能帮你理解原生 Promise 的 then 是如何工作的。

Promise 设计模式

Promise 的实现并非单一设计模式,而是多个模式的组合,每个模式解决一个核心问题,先看关键模式及对应作用:

设计模式 核心作用(Promise 中的体现) 对应实现代码(极简版 MyPromise)
状态模式 管理 Promise 的三种状态(pending/fulfilled/rejected),且状态不可逆 1. 定义 status 属性,初始为 pending;
2. resolve/reject 仅在 pending 时修改状态;
3. then 方法根据不同状态执行不同逻辑(存储回调/直接执行)。
观察者模式 解决「状态变更后通知所有回调」的问题(比如 pending 时多次调用 then,状态确定后全部执行) 1. 定义 onFulfilledCallbacks/onRejectedCallbacks 数组(存储观察者);
2. 状态变更时(resolve/reject),遍历执行数组中的回调(通知观察者)。
工厂模式 then 方法返回新的 Promise 实例(无需手动 new,由 then 内部创建),实现链式调用 1. then 内部创建 newPromise 并返回;
2. resolvePromise 辅助函数根据回调返回值「生产」新 Promise 的状态。
策略模式 允许动态传入不同的回调策略(onFulfilled/onRejected),状态变更时执行对应策略 1. then 接收两个回调参数(不同的处理策略);
2. 成功时执行 onFulfilled,失败时执行 onRejected。

微任务小迷思:then 里「push 回调到数组」和「推到微任务队列」是一回事吗

先给核心结论

  • push 回调到数组:解决「Promise 还在 pending 状态时,回调该存哪」的问题(存储逻辑);
  • 推到微任务队列:解决「回调该什么时候执行」的问题(执行时机逻辑);
  • 二者关系:push 是「保存回调」,微任务队列是「调度执行」—— 先保存,再在合适的时机丢到微任务队列执行。

一、先分清两个「队列」:回调存储数组 vs 微任务队列

这是最容易混淆的点,先明确二者的定位:

类型 作用 时机 对应 Promise 状态
回调存储数组(如 onFulfilledCallbacks 临时保存回调,避免丢失 调用 then 时,Promise 是 pending 状态 pending(异步任务未完成)
微任务队列(浏览器/Node 内置) 调度回调的执行时机,保证异步 回调准备执行时(Promise 状态确定后) fulfilled/rejected(异步任务完成)

二、分步拆解:两个操作的配合流程(结合代码)

用我们之前写的 MyPromise 代码,还原完整执行流程:

场景:异步 Promise + 调用 then
// 1. 创建异步 Promise(pending 状态,1s 后 resolve)
const p = new MyPromise((resolve) => {
  setTimeout(() => resolve(100), 1000);
});

// 2. 调用 then:此时 Promise 还是 pending,执行「push 回调到数组」
p.then(res => console.log('回调执行:', res));
步骤1:push 回调到数组(存储)
// MyPromise 的 then 方法中
if (this.status === MyPromise.PENDING) {
  // 关键:把回调逻辑包装后,push 到存储数组
  this.onFulfilledCallbacks.push(() => {
    executeCallback(onFulfilled, this.value);
  });
}
  • 此时 Promise 还在 pending(1s 后才 resolve),无法执行回调,所以先把「回调执行逻辑」push 到 onFulfilledCallbacks 数组里保存;
  • 这一步和「微任务队列」无关,只是「临时存档」。
步骤2:状态确定后,执行存储的回调 → 推到微任务队列(调度)

1s 后,调用 resolve(100),Promise 状态变为 fulfilled

// resolve 函数中
this.status = MyPromise.FULFILLED;
this.value = value;
// 遍历执行存储数组中的回调
this.onFulfilledCallbacks.forEach(callback => callback());

执行 callback() 时,会调用 executeCallback 函数:

const executeCallback = (callback, data) => {
  // 关键:用 setTimeout 模拟微任务,把回调推到微任务队列
  setTimeout(() => {
    const result = callback(data);
    resolvePromise(newPromise, result, resolve, reject);
  }, 0);
};
  • 此时才把「真正的回调执行逻辑」推到微任务队列(用 setTimeout 模拟);
  • 这一步是「调度执行时机」,保证回调异步执行,而非同步阻塞。

三、特殊场景:Promise 已完成(非 pending)

如果调用 then 时,Promise 已经是 fulfilled/rejected,就不会 push 到存储数组,而是直接把回调推到微任务队列

// MyPromise 的 then 方法中
if (this.status === MyPromise.FULFILLED) {
  // 直接执行 executeCallback → 推到微任务队列
  executeCallback(onFulfilled, this.value);
}

示例:

// Promise 立即 resolve(状态为 fulfilled)
const p = new MyPromise((resolve) => resolve(100));
// 调用 then 时,状态已确定,直接把回调推到微任务队列
p.then(res => console.log(res));

四、关键区别:用生活例子类比

把 Promise 比作「奶茶店」:

  • push 回调到数组:你点单时,奶茶还没做好(pending),店员把你的「取餐需求」(回调)记在小本本(存储数组)上,避免漏单;
  • 推到微任务队列:奶茶做好了(fulfilled),店员喊你取餐,但店里规定「先做完所有即时单(同步代码),再叫号取餐(微任务)」—— 把你的「取餐动作」排到微任务队列,按顺序执行;
  • 若你到店时,奶茶已经做好了(非 pending):店员直接把你的「取餐动作」排到微任务队列,不用记小本本。

五、总结

  1. 不是一回事
    • push 回调到数组:是「存储行为」,解决 pending 状态下回调的保存问题,和执行时机无关;
    • 推到微任务队列:是「调度行为」,解决回调的异步执行时机问题,保证符合 Promise 规范;
  2. 关联关系
    • 若 Promise 是 pending → 先 push 到存储数组,状态确定后,再从数组取出回调,推到微任务队列执行;
    • 若 Promise 已完成 → 跳过存储数组,直接把回调推到微任务队列;
  3. 核心目的
    • 存储数组:保证回调不丢失;
    • 微任务队列:保证回调异步执行,且执行顺序符合规范(微任务优先级 > 宏任务)。

记住一句话就能分清:先存(push 数组),后调(微任务队列) —— 存储是为了不丢,微任务是为了异步。

昨天 — 2026年1月17日首页
❌
❌