Promise基础知识整理,看看还有你不清楚的吗
背景
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
逐点解读(核心解释)
-
构造函数参数(兑现承诺的逻辑):
-
new Promise((resolve, reject) => { ... })中的箭头函数就是「兑现承诺的逻辑」。 - 这个函数同步执行:所以先输出
Promise构造函数的执行函数:同步执行,再执行后续的同步代码。
-
-
resolve 和 reject 参数:
二者都是浏览器内置的函数,不是我们定义的。-
resolve(100):把 Promise 状态改为fulfilled(成功),并把100作为「成功结果」传递给then的第一个回调。 -
reject(new Error('promise rejected')):把状态改为rejected(失败),并把错误对象作为「失败理由」传递给then的第二个回调。
-
状态一旦确定就不能修改:
- 代码中先调用了
resolve(100),此时 Promise 状态已经固定为成功,即便后续调用reject(哪怕取消注释),也不会改变状态,then的失败回调永远不会执行。
- 代码中先调用了
-
then 方法的回调:
-
then的第一个参数:只有 Promise 状态为fulfilled时才执行,接收resolve传递的值。 -
then的第二个参数:只有 Promise 状态为rejected时才执行,接收reject传递的错误。 - 注意:
then的回调是微任务,所以会等所有同步代码执行完后才执行(先输出同步代码:在Promise创建后执行,再输出Promise成功: 100)。
-
总结
- Promise 构造函数的执行函数是同步执行的,里面的
resolve/reject用于修改 Promise 状态(成功/失败)。 - Promise 状态一旦通过
resolve或reject确定,就永久不可修改,后续调用另一个函数也无效。 -
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 }
核心逻辑拆解(为什么链式调用能避免回调地狱)
-
ajax('urls.json').then(...)执行后,返回一个新的 Promise(记为 P1)。 - 第一个 then 的回调里
return ajax(res.userUrl),这个 ajax 调用会返回另一个 Promise(记为 P2)。 - Promise 的规则:如果 then 的回调返回一个 Promise(P2),那么 then 对应的新 Promise(P1)会「继承」P2 的状态——P2 成功,P1 就成功;P2 失败,P1 就失败。
- 第二个
then(...)其实是挂载在 P1 上的回调,而非嵌套在第一个 then 内部,所以代码是扁平的。
总结
- Promise 避免回调地狱的核心是 then 的链式调用,而非嵌套使用 then。
- 关键规则:
then会返回新 Promise,若 then 回调返回 Promise,则新 Promise 继承该 Promise 的状态。 - 链式调用+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回调的返回值
});
拆解逻辑(对应你那段话):
- 「不断链式调用 then」→ 代码中
p1.then() → p2.then() → p3.then()就是链式调用。 - 「每一个 then 方法,都是为上一个 then 返回的 Promise 对象加回调」:
-
p2.then(...)→ 是给「p1.then() 返回的 p2」加回调; -
p3.then(...)→ 是给「p2.then() 返回的 p3」加回调; - 每个 then 都不是给原始的 p1 加回调,而是给「上一个 then 生成的新 Promise」加回调。
-
- 「状态明确过后的回调」:只有当被绑定的 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 都绑定在同一个对象上,状态只能变一次。
总结
- Promise 链式调用≠传统链式调用:传统是返回
this(同一对象),Promise 是返回全新的 Promise 对象。 - 链式调用的本质:每个
then都是给「上一个then返回的新 Promise」绑定回调,而非给原始 Promise 绑定。 - 返回新 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 的失败
关键细节:
-
then(成功回调, 失败回调)的失败回调 → 只绑定 P1,只能处理 P1 的异常(比如 task1 失败); -
catch()→ 绑定 P2,能处理 P2 的异常(包括 P2 自身失败、或 P1 未被捕获的异常向后传递过来); - 异常传递规则:如果一个 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 的异常会逃逸(未捕获)。
总结
-
catch()是then(undefined, 失败回调)的语法糖,但绑定的是上一个 then 返回的新 Promise,而非原始 Promise; -
then(成功, 失败)的失败回调仅绑定当前 Promise,无法捕获后续 then 中返回的新 Promise 异常; - 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中未被手动捕获的异常'));
});
二、为什么强烈不推荐全局捕获?(核心原因)
「不推荐全局统一处理」,核心问题有这几点:
- 调试困难:全局捕获会「兜底」所有未处理的异常,但无法精准定位异常发生的位置——一个大型项目中,你无法从全局回调里快速知道是哪一行代码、哪个异步任务抛出的异常。
- 掩盖问题:全局捕获会让开发者产生「反正有兜底,不用手动写 catch」的惰性,导致代码中大量异常没有被「针对性处理」(比如某个接口失败需要重试,另一个需要提示用户,全局捕获只能统一打印,无法差异化处理)。
- 不可控性:全局事件是「最后一道防线」,若代码中漏写 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);
});
总结
- 全局
unhandledrejection/unhandledRejection是「兜底方案」,仅适合临时调试或紧急补救,不推荐作为常规异常处理方式; - 最佳实践是「显式捕获」:在 Promise 链条末尾加
catch,针对不同异常做「差异化处理」(重试、兜底、提示用户等); - 若需要保留链条执行,可在单个异步任务后紧跟 catch,返回兜底值,避免整个链条中断。
-
关于catch返回值:- 如果 catch 回调返回正常值(普通值 / 成功的 Promise)→ 新 Promise 状态为
fulfilled(成功); - 如果 catch 回调抛出异常 / 返回失败的 Promise → 新 Promise 状态为
rejected(失败)
- 如果 catch 回调返回正常值(普通值 / 成功的 Promise)→ 新 Promise 状态为
- 如果 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() 的优势在于:
- 简化代码:创建已成功的 Promise 时,少写嵌套的执行函数,代码更简洁;
-
统一接口:当你不确定一个值是普通值还是 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); });
总结
-
Promise.resolve(value)是创建状态为 fulfilled 的 Promise 的快捷方式,等价于new Promise((resolve) => resolve(value)); - 传入普通值时,该值会作为 Promise 的成功结果,在
then的成功回调中获取; - 传入 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 对象直接作为失败理由。
总结
-
Promise.resolve(x)规则:- x 是普通值 → 返回 fulfilled 状态的 Promise,x 为成功结果;
- x 是 Promise → 原样返回 x;
- x 是 thenable 对象 → 转换成原生 Promise,执行其 then 方法。
-
Promise.reject(reason)规则:- 无论 reason 是普通值、Error 对象、甚至 Promise 对象,都会直接作为「失败理由」,返回 rejected 状态的 Promise;
- 推荐传入
Error对象(而非字符串),便于调试(包含错误堆栈)。
-
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 的回调属于「微任务」:微任务的执行规则是「当前宏任务执行完毕后,立即清空所有微任务队列,再执行下一个宏任务/渲染/事件」。
-
递归逻辑:
- 第一次调用
promiseRecursion(),Promise.resolve()生成微任务 A; - 当前宏任务执行完,执行微任务 A → 打印日志,调用
promiseRecursion()→ 生成微任务 B; - 微任务 A 执行完,立即执行微任务 B → 打印日志,生成微任务 C;
- 这个过程无限循环,微任务队列永远有新任务,主线程被微任务「占满」,没有任何时间片分配给:
- DOM 渲染(页面卡死);
- 鼠标点击/滚动等事件响应(交互失效);
- 其他宏任务(比如 setTimeout、网络请求)。
- 第一次调用
- 调用栈角度:虽然每次 then 回调执行完会清空当前调用栈,但微任务的「连续执行」让主线程没有「空闲期」,本质是「无限的同步执行流」。
2. setTimeout 递归:宏任务「排队式」执行,每次释放主线程
- setTimeout 的回调属于「宏任务」:宏任务的执行规则是「执行完一个宏任务后,先执行所有微任务,再处理渲染,再取下一个宏任务」。
-
递归逻辑:
- 第一次调用
timeoutRecursion(),setTimeout把回调 A 加入「宏任务队列」; - 当前宏任务执行完,执行微任务 → 渲染页面 → 处理事件(点击/滚动)→ 再执行宏任务 A;
- 宏任务 A 执行:打印日志,调用
timeoutRecursion()→ 把回调 B 加入宏任务队列; - 宏任务 A 执行完,主线程会「释放」,先处理渲染、事件响应,再执行下一个宏任务 B;
- 这个过程虽然无限,但每次宏任务执行完都会给主线程喘息机会,页面渲染、事件响应能正常进行,因此不会卡死。
- 第一次调用
补充:为什么 Promise 微任务要「立即执行」?
微任务的设计初衷是「处理异步但需要尽快完成的逻辑」(比如 Promise 回调、async/await),优先级高于宏任务和渲染,这保证了异步逻辑的执行顺序,但无限递归的微任务会滥用这个优先级,导致主线程阻塞。
总结
- 核心差异:Promise 递归是「微任务无限连续执行」,主线程无喘息机会;setTimeout 递归是「宏任务排队执行」,每次执行后释放主线程,允许渲染/事件响应。
- 调用栈/队列:Promise 递归让微任务队列永远非空,主线程被占满;setTimeout 递归的宏任务队列虽有任务,但每次执行完会处理渲染和事件。
- 本质:浏览器卡死的核心是「主线程无法处理渲染/交互」,而非「递归本身」——setTimeout 递归给了主线程处理这些的时间,而 Promise 递归没有。
如果想让 Promise 递归不卡死,可在递归中加入 setTimeout 「让出主线程」:
// 改进版 Promise 递归:不卡死
function promiseRecursion() {
Promise.resolve().then(() => {
console.log("Promise 递归执行");
setTimeout(() => promiseRecursion(), 0); // 用setTimeout让出主线程
});
}
promiseRecursion();
Promise 的 then 方法的核心实现
先明确 then 方法的核心需求
-
then接收两个参数:onFulfilled(成功回调)、onRejected(失败回调); - 回调需异步执行(微任务,这里用
setTimeout模拟); - 若 Promise 状态未确定(pending),需先存储回调;若已确定,直接执行回调;
-
then必须返回新的 Promise,实现链式调用; - 上一个
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。
- 返回普通值 → 新 Promise 状态为
测试代码(验证 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);
}
总结
-
then方法的核心是「状态判断 + 回调存储/执行 + 返回新 Promise」; - 异步执行回调、状态不可逆、链式调用(返回新 Promise)是
then的三大关键特性; - 这个极简实现去掉了复杂的边界处理(如 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):店员直接把你的「取餐动作」排到微任务队列,不用记小本本。
五、总结
-
不是一回事:
-
push 回调到数组:是「存储行为」,解决 pending 状态下回调的保存问题,和执行时机无关; -
推到微任务队列:是「调度行为」,解决回调的异步执行时机问题,保证符合 Promise 规范;
-
-
关联关系:
- 若 Promise 是 pending → 先 push 到存储数组,状态确定后,再从数组取出回调,推到微任务队列执行;
- 若 Promise 已完成 → 跳过存储数组,直接把回调推到微任务队列;
-
核心目的:
- 存储数组:保证回调不丢失;
- 微任务队列:保证回调异步执行,且执行顺序符合规范(微任务优先级 > 宏任务)。
记住一句话就能分清:先存(push 数组),后调(微任务队列) —— 存储是为了不丢,微任务是为了异步。