主题
实现 then 方法:状态改变才能执行回调与实现 then (Day18 2/3)
本页对应 Day18-自定义Promise,共 5 段视频,约 1 小时 33 分。
学习目标
- 能够说明
then多次调用时多个回调的执行规则,并通过将回调存入数组、状态改变后遍历执行,验证多个成功/失败回调均被调用。 - 能够实现
then返回新的 Promise 实例,并针对回调返回非 Promise、返回 Promise、抛出异常等情况正确设置新实例的状态与结果。 - 能够将成功回调和失败回调的重复处理逻辑抽离为统一处理函数,并在状态已改变、状态未改变及回调列表执行三种路径中复用。
- 能够为
then的参数设置默认值,使未传参或传入非函数参数时状态与结果继续向下传递,并通过用例验证异常穿透效果。 - 能够通过链式调用、链式中断、错误向下传递等测试用例,检验自定义
then方法的链式调用与异常穿透能力。
知识点
为什么需要“状态改变才执行回调”
Promise 的 then() 不是“调用时立刻拿到结果”,而是“登记两个回调,等结果出来再执行”。所以本节要解决的核心问题是:调用 then() 时,实例可能已经成功/失败,也可能还处于 pending。如果已经改变,就按状态选回调执行;如果还没改变,就先把回调存起来,等 resolve/reject 改状态时再执行。
怎么想:把 then() 看成“登记处”,把状态看成“闸门”。闸门已开,直接走对应通道;闸门未开,先登记,等闸门打开再放行。
怎么做:构造函数里的 changeResolved/changeRejected 负责改状态、存结果,并尝试调用已登记的回调;then() 里根据 #state 分流。
易错点:状态只能从 pending 改一次;回调要异步执行;未决时保存回调不能只保存一个对象,否则多个 then() 会互相覆盖。
怎么做:按笔记代码落地
下面保留本节笔记中的代码。注意其中 #callbackObj 是对象,只能保存“最后一组”回调;转写稿后面会通过调试发现这个问题。
js
(window => {
class Promise {
// 设置私有属性,记录实例的状态和结果
#state = 'pendding';
#result;
// 设置私有属性,保存then传入的回调函数
#callbackObj = {};
/**
* 构造器方法,实例化的时候自动执行
* @param {Function} exector 执行器函数,实例化的时候自动执行
*/
constructor(exector) {
// 执行该函数,可将实例状态更改为成功
const changeResolved = value => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'fulfilled';
this.#result = value;
// 如果这个时候 then() 已经将成功的回调传入,直接调用
this.#callbackObj.onResolved?.(value);
};
// 执行该函数,可将实例状态更改为成功
const changeRejected = reason => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'rejected';
this.#result = reason;
// 如果这个时候 then() 已经将失败的回调传入,直接调用
this.#callbackObj.onRejected?.(reason);
};
// 调用执行器函数, 如果执行器函数中有异常,修改状态为失败
try {
exector(changeResolved, changeRejected);
} catch (error) {
// 更改为失败状态,错误对象作为结果
changeRejected(error);
}
}
/**
* 给实例传递成功和失败的回调函数,该方法会添加到实例的原型上
*/
then(onResolved, onRejected) {
if (this.#state === 'fulfilled') { // 当传入回调函数时,实例的状态已经改为成功
setTimeout(() => {
onResolved(this.#result);
});
} else if (this.#state === 'rejected') { // 当传入回调函数时,实例的状态已经改为失败
setTimeout(() => {
onRejected(this.#result);
})
} else { // 当传入回调函数时,状态还未改变
// 将两个回调函数保存到私有属性中
this.#callbackObj = {
onResolved,
onRejected
}
}
}
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
*/
catch() {
}
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
*/
static resolve() {
}
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
*/
static reject() {
}
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static all() {
}
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static race() {
}
}
window.Promise = Promise;
})(window || global);这段代码对应三个动作:then() 传两个回调;调用时若已成功/失败,就按状态选一个执行;若未决,就把回调存进 #callbackObj,等 changeResolved/changeRejected 改状态时再调用。
调试:多个 then 为什么会覆盖
转写稿里演示了内置 Promise:对同一个 p1 连续写三个 then,状态成功后“成功1、成功2、成功3”都会执行;换成本节自定义版本,却只有最后一个执行。原因不是状态判断错了,而是 #callbackObj = { onResolved, onRejected } 每次都会把前一次保存的对象覆盖掉。
怎么想:then() 可以被调用多次,未决阶段应该“登记多组回调”,而不是“保存最后一组”。
怎么做:把 #callbackObj 改成回调列表,例如 #callbackList = [];未决时 push({ onResolved, onRejected });changeResolved 里遍历列表执行 onResolved,changeRejected 里遍历执行 onRejected。
易错点:如果状态在 then() 之前已经改变,走的是 setTimeout 分支,每次 then() 都会各自执行,所以看起来没问题;真正出问题的是“先 then() 后改状态”的场景。
易错点
#state初始值转写稿读作pendding,标准写法是pending;判断时前后要一致。changeRejected的注释应写“更改为失败”,不要照抄成“成功”。then()已决分支用setTimeout包回调,是为了模拟 Promise 回调异步执行;不要改成同步直接调用。- 状态改变函数里必须先判断
pending,否则resolve后再reject会覆盖结果。 - 本节只实现“状态改变才执行回调”,
then()返回新 Promise、链式调用、异常穿透是后续内容,不要在本节代码里假装已完成。
先分清:多个回调不是链式调用
为什么需要:原生 Promise 的同一个实例可以多次调用 then,每个 then 传入的回调都会被执行。笔记里的三次 p1.then(...) 操作的都是同一个 p1,这不是 p1.then(...).then(...) 的链式调用。
怎么想:先验证行为。转写稿中先把自定义 Promise 注释掉,用内置 Promise 跑三次 then,能看到“成功1/成功2/成功3”都执行;说明同一实例的多个回调应当全部保留。换成自定义版本后,最初只执行最后一次,原因是之前用单个对象保存回调,后一次把前一次覆盖了。
怎么做:示例代码保留如下:
js
// 实例化 Promise
const p1 = new Promise((resolve, reject) => {
// 修改为失败状态
setTimeout(() => {
reject('Error');
}, 2000)
});
// 调用then() 方法,传入回调函数
p1.then(val => {
console.log('成功1!', val);
}, reason => {
console.log('失败1!', reason);
});
p1.then(val => {
console.log('成功2!', val);
}, reason => {
console.log('失败2!', reason);
});
p1.then(val => {
console.log('成功3!', val);
}, reason => {
console.log('失败3!', reason);
});易错点:不要把“同一实例多次 then”说成链式调用;链式调用看的是 then 的返回值,本节还没有实现返回新 Promise。
把单个回调对象改成回调列表
为什么需要:同一个 Promise 实例可能被 then 多次,每次都要记住一对成功/失败回调。只存一个对象,后一次会覆盖前一次,导致前面的回调丢失。
怎么想:把“保存一对回调”改成“保存多对回调”。转写稿里明确把原来的回调对象改成数组:每调用一次 then,就往数组里追加一个 { onResolved, onRejected }。这样调用三次就存三对,状态改变后逐个执行。
怎么做:在类中设置私有属性 #callbackList = [];then 在 pending 阶段执行:
js
this.#callbackList.push({
onResolved,
onRejected
});易错点:数组要按调用顺序 push,不要覆盖;状态名按笔记代码保留为 pendding,不要随意改成别的拼写导致判断失效。
状态改变时遍历执行回调
为什么需要:回调先被存起来,等 resolve 或 reject 真正执行时,必须把列表里的回调全部跑一遍,否则多个 then 仍然只有部分生效。
怎么想:状态改变函数里不仅要改 #state 和 #result,还要遍历 #callbackList。成功时调用每个成员的 onResolved(value),失败时调用每个成员的 onRejected(reason)。
怎么做:核心代码如下:
js
this.#callbackList.forEach(callback => {
callback.onResolved(value);
});失败分支同理调用 callback.onRejected(reason)。
易错点:成功分支不要误调 onRejected,失败分支不要误调 onResolved;遍历前不需要额外判断列表是否为空,空数组 forEach 不会执行。
then 的两种执行时机
为什么需要:调用 then 时,Promise 状态可能已经改变,也可能还在 pending。两种时机处理方式不同,但最终都要保证回调异步执行。
怎么想:如果 then 被调用时状态已是 fulfilled 或 rejected,说明结果已定,用 setTimeout 异步执行对应回调;如果还是 pendding,先把回调存入 #callbackList,等状态改变时再由遍历逻辑执行。
怎么做:then 中先判断 this.#state:成功走 setTimeout(() => onResolved(this.#result)),失败走 setTimeout(() => onRejected(this.#result)),否则 push 到回调列表。
易错点:不要在 then 里同步直接调用回调;Promise 的回调应当异步执行。转写稿调试时也强调:then 本身是同步调用的,但传入的回调要等一等再执行。
调试过程与完整实现
转写稿的调试顺序是:先用内置 Promise 验证三个 then 都会执行;再打开自定义实现,发现只执行最后一个,定位到“单个对象被覆盖”;改成 #callbackList 数组后,用 2 秒后 reject('Error') 测试,失败1、失败2、失败3 都能输出。若在调用 then 前状态已经改变,then 会走已改变分支并异步执行;若状态未改,则先入数组,等状态改变后统一遍历。
完整实现代码保留如下:
js
(window => {
class Promise {
// 设置私有属性,记录实例的状态和结果
#state = 'pendding';
#result;
// 设置私有属性,保存 then() 传入的回调函数
#callbackList = [];
/**
* 构造器方法,实例化的时候自动执行
* @param {Function} exector 执行器函数,实例化的时候自动执行
*/
constructor(exector) {
// 执行该函数,可将实例状态更改为成功
const changeResolved = value => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'fulfilled';
this.#result = value;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onResolved(value);
});
};
// 执行该函数,可将实例状态更改为成功
const changeRejected = reason => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'rejected';
this.#result = reason;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onRejected(reason);
});
};
// 调用执行器函数, 如果执行器函数中有异常,修改状态为失败
try {
exector(changeResolved, changeRejected);
} catch (error) {
// 更改为失败状态,错误对象作为结果
changeRejected(error);
}
}
/**
* 给实例传递成功和失败的回调函数,该方法会添加到实例的原型上
*/
then(onResolved, onRejected) {
if (this.#state === 'fulfilled') { // 当传入回调函数时,实例的状态已经改为成功
setTimeout(() => {
onResolved(this.#result);
});
} else if (this.#state === 'rejected') { // 当传入回调函数时,实例的状态已经改为失败
setTimeout(() => {
onRejected(this.#result);
})
} else { // 当传入回调函数时,状态还未改变
// 将两个回调函数保存到私有属性中
this.#callbackList.push({
onResolved,
onRejected
});
}
}
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
*/
catch() {
}
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
*/
static resolve() {
}
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
*/
static reject() {
}
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static all() {
}
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static race() {
}
}
window.Promise = Promise;
})(window || global);易错点:本节代码默认调用者会传两个回调;如果只传成功回调而实例失败,onRejected 不存在时调用会抛错。默认值处理和真正的异常穿透属于后续内容,本节先把“多个回调都能保存、都能执行”这条主线跑通。
先明确目标:then 要返回一个新的 Promise
为什么需要:如果 then() 不返回值,链式调用 p.then(...).then(...) 会直接报错,因为第二个 then 拿不到 Promise 实例。
怎么想:then() 必须 return new Promise(...),这个新 Promise 的状态不能立刻定死,而要等传入的成功/失败回调执行完再决定。
怎么做:在 then() 内部创建新 Promise,把回调执行结果 res 分情况处理:非 Promise 就 resolve(res);返回 Promise 就让 res.then(resolve, reject);回调抛异常就 reject(error)。
易错点:不要以为 new Promise 一创建状态就确定了;它要等回调执行结果。笔记里阶段代码的 else 分支只把回调存进 #callbackList,还没有处理返回 Promise 的状态,所以链式调用会断。
多个回调要用数组保存
为什么需要:同一个实例可以多次调用 then,内置 Promise 会让多个成功/失败回调都执行。
怎么想:早期用对象保存回调,后一次会覆盖前一次,导致只执行最后一个;应改成数组 #callbackList。
怎么做:#callbackList.push({ onResolved, onRejected }),状态改变时 forEach 遍历执行。
易错点:这不是链式调用,只是同一个 Promise 上注册多个回调;链式调用看的是 then() 的返回值。
本节阶段代码:返回新 Promise 并保存回调
js
(window => {
class Promise {
// 设置私有属性,记录实例的状态和结果
#state = 'pendding';
#result;
// 设置私有属性,保存 then() 传入的回调函数
#callbackList = [];
/**
* 构造器方法,实例化的时候自动执行
* @param {Function} exector 执行器函数,实例化的时候自动执行
*/
constructor(exector) {
// 执行该函数,可将实例状态更改为成功
const changeResolved = value => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'fulfilled';
this.#result = value;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onResolved(value);
});
};
// 执行该函数,可将实例状态更改为成功
const changeRejected = reason => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'rejected';
this.#result = reason;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onRejected(reason);
});
};
// 调用执行器函数, 如果执行器函数中有异常,修改状态为失败
try {
exector(changeResolved, changeRejected);
} catch (error) {
// 更改为失败状态,错误对象作为结果
changeRejected(error);
}
}
/**
* 给实例传递成功和失败的回调函数,该方法会添加到实例的原型上
*/
then(onResolved, onRejected) {
// 返回 Promise 对象
return new Promise((resolve, reject) => {
if (this.#state === 'fulfilled') { // 当传入回调函数时,实例的状态已经改为成功
setTimeout(() => {
try {
// 执行 then() 传入的回调函数并获取到返回值
const res = onResolved(this.#result);
// 判断返回值res是否是Promise的实例
if (res instanceof Promise) {
// then()返回值的状态与res一致 情况三
res.then(resolve, reject);
// res.then(value => {
// resolve(value);
// }, reason => {
// reject(reason);
// })
} else {
// 改为成功状态,将返回值作为结果 情况一 情况二
resolve(res);
}
} catch (error) {
// 如果调用 then() 传入的回调函数的时候出现异常,改为失败状态 情况四
reject(error);
}
});
} else if (this.#state === 'rejected') { // 当传入回调函数时,实例的状态已经改为失败
setTimeout(() => {
try {
// 调用 then() 传入的回调函数并得到返回值
const res = onRejected(this.#result);
// 判断返回值的类型
if (res instanceof Promise) {
// 情况三 then()返回值的状态与res的状态一致
res.then(resolve, reject);
} else {
// 情况一 情况二: 设置为成功状态
resolve(res);
}
} catch (error) {
// 情况四:调用传入的回调函数时出现异常
reject(error);
}
})
} else { // 当传入回调函数时,状态还未改变
// 将两个回调函数保存到私有属性中
this.#callbackList.push({
onResolved,
onRejected
});
}
})
}
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
*/
catch() {
}
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
*/
static resolve() {
}
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
*/
static reject() {
}
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static all() {
}
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static race() {
}
}
window.Promise = Promise;
})(window || global);调试链式调用断掉的原因
为什么需要:返回 new Promise 只解决“能调用下一个 then”,不解决“下一个 then 什么时候执行”。
怎么想:如果原 Promise 状态已经成功/失败,回调会立即执行,新 Promise 可通过 resolve/reject 改状态;如果原 Promise 还没改状态,回调被放进 #callbackList,等状态改变时才执行。
怎么做:转写稿中的调试结论是:else 里只 push 原回调,状态改变时只是“干巴巴执行”回调,没有把 then() 返回的新 Promise 状态改掉。解决方向是把成功/失败分支中“执行回调并处理返回值”的逻辑抽成 handle,else 中也包装成 () => handle(onResolved)、() => handle(onRejected),保证回调最终都走统一处理。
易错点:handle 必须写在 new Promise 内部,才能拿到 resolve/reject;如果原 Promise 不是立即改状态,回调进入回调列表,没封装就会导致后续链式调用卡住。
参数默认值:实现异常穿透
为什么需要:用户可能不传 then 的第二个参数,或传入非函数;内置 Promise 会继续向下传递状态和结果。
怎么想:如果 onResolved 不是函数,就默认成 value => value,让成功结果继续向下传;如果 onRejected 不是函数,就默认成 reason => { throw reason; },让失败原因继续向下抛。
怎么做:在 then 开头判断 typeof onResolved !== 'function'、typeof onRejected !== 'function' 并赋默认函数。
易错点:失败默认函数不能写成返回值,否则会把失败“变成成功”;必须抛出 reason,后续 catch 才能接住,这才是真正的异常穿透。
从重复代码里抽出 handler
为什么需要:then 在“状态已成功”“状态已失败”“状态还未改变”三种时机下都要执行回调,并且执行完还要根据返回值修改新 Promise 的状态。如果三处各写一遍 try/catch、判断返回值、调用 resolve/reject,代码会高度重复,也容易漏改。
怎么想:把“执行某个回调,并根据它的返回值或异常去改变新 Promise 状态”这件事抽成一个函数。这个函数只关心“要执行哪个回调”,不关心它是成功回调还是失败回调。
怎么做:在 then 返回的 new Promise((resolve, reject) => { ... }) 内部定义 handler(cb)。handler 里用 try 包住 cb(this.#result),拿到返回值 res;如果 res instanceof Promise,就 res.then(resolve, reject),让新 Promise 与 res 状态一致;否则直接 resolve(res);若执行回调抛异常,则 reject(error)。
易错点:handler 必须写在 new Promise 的执行器内部,才能拿到这里的 resolve/reject;写到外面就找不到它们。另外 this.#result 能取到当前实例的结果,是因为 handler 是箭头函数,this 沿用 then 里的 Promise 实例。
多个 then 回调要存成数组
为什么需要:同一个 Promise 可以多次调用 then,例如 p.then(成功1, 失败1); p.then(成功2, 失败2);。这不是链式调用,而是给同一个实例注册多个回调。之前用一个对象保存回调,后一次会覆盖前一次,导致只有最后一个回调执行。
怎么想:每次 then 都应该把本次的两个回调保存下来,等状态改变时全部执行。能按顺序追加的数据结构就是数组。
怎么做:把私有属性改成 #callbackList = []。当 then 被调用时状态还是 pendding,就 this.#callbackList.push({ onResolved: () => handler(onResolved), onRejected: () => handler(onRejected) })。构造器里状态改变时用 forEach 遍历列表,分别调用 callback.onResolved() 或 callback.onRejected()。
易错点:这里保存的不能是原始回调,而要是“经过 handler 包装后的函数”。否则状态改变时只是干巴巴执行回调,没有把返回值交给新 Promise 的 resolve/reject,链式调用会断。
then 返回新 Promise 才能链式调用
为什么需要:p.then(...).then(...) 能继续调用,是因为第一个 then 返回了一个新的 Promise,而不是返回 undefined。
怎么想:then 内部直接 return new Promise((resolve, reject) => { ... })。这个新 Promise 的状态由“本次传入的回调执行结果”决定。
怎么做:状态已是 fulfilled 时,setTimeout(() => handler(onResolved));状态已是 rejected 时,setTimeout(() => handler(onRejected));状态还是 pendding 时,把包装后的回调推进 #callbackList。
易错点:new Promise 的执行器是同步执行的,但 handler 里改新 Promise 状态要等回调真正执行。用 setTimeout 模拟异步,是为了让 then 里的回调异步执行。还要注意:如果回调返回一个永远不改变状态的 Promise,后续链式调用就会中断,这正是“调用中断”的来源。
pending 分支必须走 handler 的调试过程
为什么需要:只处理“状态已改变”的两个分支还不够。转写稿里调试时发现:当 P1 不是立即改状态,而是两秒后改,then 传入的回调会先进入 #callbackList;等构造器里 forEach 执行时,如果列表里存的是原始回调,就只执行了回调,没有用它的返回值去改 then 返回的新 Promise,于是第二个 then 后面的链式调用卡住。
怎么想:无论状态是同步已改,还是异步未改,最终都要走同一套“执行回调 + 处理返回值/异常”的逻辑。所以 pending 分支保存的 onResolved/onRejected 内部也要调用 handler。
怎么做:把 pending 分支改成保存 onResolved: () => handler(onResolved)、onRejected: () => handler(onRejected)。这样状态改变后遍历列表,仍然会进入 handler,新 Promise 才能被 resolve/reject。
易错点:不要把这套逻辑只写在 if (fulfilled) 和 else if (rejected) 里;else 分支漏掉 handler,异步场景下链式调用就会断。转写稿中反复强调“加入回调列表的也要经过它处理”,指的就是这一点。
完整代码
js
(window => {
class Promise {
// 设置私有属性,记录实例的状态和结果
#state = 'pendding';
#result;
// 设置私有属性,保存 then() 传入的回调函数
#callbackList = [];
/**
* 构造器方法,实例化的时候自动执行
* @param {Function} exector 执行器函数,实例化的时候自动执行
*/
constructor(exector) {
// 执行该函数,可将实例状态更改为成功
const changeResolved = value => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'fulfilled';
this.#result = value;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onResolved(value);
});
};
// 执行该函数,可将实例状态更改为成功
const changeRejected = reason => {
// 如果状态已经更改,结束该函数
if (this.#state !== 'pendding') {
return;
}
// 修改状态和结果
this.#state = 'rejected';
this.#result = reason;
// 遍历回调函数列表
this.#callbackList.forEach(callback => {
callback.onRejected(reason);
});
};
// 调用执行器函数, 如果执行器函数中有异常,修改状态为失败
try {
exector(changeResolved, changeRejected);
} catch (error) {
// 更改为失败状态,错误对象作为结果
changeRejected(error);
}
}
/**
* 给实例传递成功和失败的回调函数,该方法会添加到实例的原型上
*/
then(onResolved, onRejected) {
// 返回 Promise 对象
return new Promise((resolve, reject) => {
// 封装函数 专门用于执行传入 then() 的回调
const handler = cb => {
try {
// 执行 then() 传入的回调函数并获取到返回值
const res = cb(this.#result);
// 判断返回值res是否是Promise的实例
if (res instanceof Promise) {
// then()返回值的状态与res一致 情况三
res.then(resolve, reject);
} else {
// 改为成功状态,将返回值作为结果 情况一 情况二
resolve(res);
}
} catch (error) {
// 如果调用 then() 传入的回调函数的时候出现异常,改为失败状态 情况四
reject(error);
}
};
// 当传入回调函数时,实例的状态已经改为成功
if (this.#state === 'fulfilled') {
setTimeout(() => {
handler(onResolved);
});
// 当传入回调函数时,实例的状态已经改为失败
} else if (this.#state === 'rejected') {
setTimeout(() => {
handler(onRejected);
})
// 当传入回调函数时,状态还未改变
} else {
// 将两个回调函数保存到私有属性中
this.#callbackList.push({
onResolved: () => {
handler(onResolved);
},
onRejected: () => {
handler(onRejected);
}
});
}
})
}
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
*/
catch() {
}
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
*/
static resolve() {
}
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
*/
static reject() {
}
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static all() {
}
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例,该方法添加到类本身(静态方法)
*/
static race() {
}
}
window.Promise = Promise;
})(window || global);问题从哪里来:不传回调时状态没有继续传递
then(onResolved, onRejected) 正常用法是传两个函数。但实际使用时经常只传成功回调,或者干脆不传,甚至传数字。内置 Promise 的处理是:如果参数不是函数,就当它不存在,让当前 Promise 的状态和结果继续向下传递。
自定义 Promise 如果直接执行 onRejected(this.#result),而 onRejected 是 undefined,就会抛 TypeError。这个错误被 handler 的 try/catch 捕获后,then 返回的新 Promise 会变成失败,但失败原因是 TypeError,不是上一步真正的失败原因。这样只实现了“异常行为向下传”,没有实现“结果和原因向下传”,也就是异常穿透不完整。
怎么想:把缺失的回调补成默认函数
核心思路是在 then 开头判断参数类型。不是函数就赋默认值:
- 成功回调默认写成
value => value:收到什么结果就返回什么结果,后面resolve(res)会让新 Promise 成功,结果继续向下传。 - 失败回调默认写成
reason => { throw reason; }:把上一步失败原因重新抛出,handler里的try/catch捕获后执行reject(error),新 Promise 失败,原因仍是原来的reason。
注意失败默认值不能写成 reason => reason,否则会把失败变成成功。
怎么做:在 then 开头设置参数默认值
本节修改后的 then 如下,constructor 中 #callbackList 的遍历逻辑保持原样:
js
then(onResolved, onRejected) {
// 如果传入的参数不是函数,给参数设置默认值
if (typeof onResolved !== 'function') {
onResolved = value => value;
}
if (typeof onRejected !== 'function') {
onRejected = reason => {
throw reason;
};
}
return new Promise((resolve, reject) => {
// 封装函数,专门用于执行传入 then() 的回调
const handler = cb => {
try {
// 执行 then() 传入的回调函数并获取返回值
const res = cb(this.#result);
// 判断返回值 res 是否是 Promise 的实例
if (res instanceof Promise) {
// then() 返回值的状态与 res 一致
res.then(resolve, reject);
} else {
// 改为成功状态,将返回值作为结果
resolve(res);
}
} catch (error) {
// 回调执行异常,改为失败状态
reject(error);
}
};
if (this.#state === 'fulfilled') {
setTimeout(() => {
handler(onResolved);
});
} else if (this.#state === 'rejected') {
setTimeout(() => {
handler(onRejected);
});
} else {
// 状态未改变,把两个回调保存到私有属性中
this.#callbackList.push({
onResolved: () => {
handler(onResolved);
},
onRejected: () => {
handler(onRejected);
}
});
}
});
}这里默认值写在 then 开头,而不是写在某个分支里,所以成功、失败、pending 三种情况都会使用补好的函数。handler 内部通过 cb(this.#result) 取当前结果,因此 #callbackList 里保存的包装函数调用时不需要再传结果。
调试过程:只传一个、不传、传数字
转写稿里先让 P1 变成失败状态,然后只传成功回调。原代码执行到失败分支时发现 onRejected 不存在,调用报错,后面拿到的是 TypeError。加上默认值后,失败回调默认抛出原 reason,所以“失败二”拿到的是上一步真正的原因。
接着测试完全不传参数。内置 Promise 会把成功结果或失败原因继续向下传;自定义版本加上默认值后也能做到:成功时默认函数返回 value,新 Promise 成功且结果不变;失败时默认函数 throw reason,新 Promise 失败且原因不变。
再测试传 10、20 这种非函数参数。typeof 判断结果不是 'function',同样赋默认值,所以效果和不传参数一致。
易错点
- 必须用
typeof onResolved !== 'function'判断,不能只判断undefined,因为数字、对象等都不是函数。 - 失败默认函数必须
throw reason,不能return reason。 - 默认值要放在
then开头,保证 pending 分支加入#callbackList时也已经使用默认函数。 handler的try/catch要包住回调执行和返回值判断,否则回调里的异常不会让新 Promise 失败。- 默认值只影响
then返回的新 Promise,不会改变原 Promise 的状态;真正的异常穿透最终仍应由catch接住处理。
课堂画面





代码示例
示例一:then 支持传入多个回调(回调存入数组)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>自定义 Promise —— then 支持传入多个回调</title>
</head>
<body>
<pre id="log"></pre>
<script>
(window => {
class Promise {
// 私有属性:记录实例的状态和结果
#state = 'pendding';
#result;
// 私有属性:保存 then() 传入的回调函数(数组,可存多个)
#callbackList = [];
constructor(exector) {
// 执行该函数,可将实例状态更改为成功
const changeResolved = value => {
if (this.#state !== 'pendding') return;
this.#state = 'fulfilled';
this.#result = value;
// 遍历回调函数列表,全部执行
this.#callbackList.forEach(callback => {
callback.onResolved(value);
});
};
// 执行该函数,可将实例状态更改为失败
const changeRejected = reason => {
if (this.#state !== 'pendding') return;
this.#state = 'rejected';
this.#result = reason;
this.#callbackList.forEach(callback => {
callback.onRejected(reason);
});
};
// 调用执行器函数,异常则改为失败
try {
exector(changeResolved, changeRejected);
} catch (error) {
changeRejected(error);
}
}
then(onResolved, onRejected) {
if (this.#state === 'fulfilled') { // 调用时状态已成功
setTimeout(() => {
onResolved(this.#result);
});
} else if (this.#state === 'rejected') { // 调用时状态已失败
setTimeout(() => {
onRejected(this.#result);
});
} else { // 调用时状态未改变
this.#callbackList.push({ onResolved, onRejected });
}
}
catch() {}
static resolve() {}
static reject() {}
static all() {}
static race() {}
}
window.Promise = Promise;
})(window || global);
const out = msg => {
document.getElementById('log').textContent += msg + '\n';
};
// 实例化 Promise:2 秒后改为失败状态
const p1 = new Promise((resolve, reject) => {
setTimeout(() => { reject('Error'); }, 2000);
});
// 对同一个实例调用三次 then,三对回调都要存下来
p1.then(val => out('成功1!' + val), reason => out('失败1!' + reason));
p1.then(val => out('成功2!' + val), reason => out('失败2!' + reason));
p1.then(val => out('成功3!' + val), reason => out('失败3!' + reason));
</script>
</body>
</html>要点解释 / 易错点
常见错误
- 现象:连续调用多个
then,只有最后一个回调执行。原因:用#callbackObj单对象保存回调,后一次覆盖前一次。修法:改为#callbackList数组,push保存,状态变更时forEach派发。 - 现象:
then回调输出顺序与原生 Promise 不一致。原因:状态已定时同步执行回调。修法:状态已定分支用setTimeout包裹handler。 - 现象:
p.then(...).then(...)报undefined相关错误。原因:then没有返回新 Promise。修法:then内return new Promise((resolve, reject) => { ... })。 - 现象:回调返回 Promise 时,链式下一环拿到 Promise 对象或状态不同步。原因:直接
resolve(res),未判断返回值类型。修法:res instanceof Promise ? res.then(resolve, reject) : resolve(res)。 - 现象:回调抛异常后返回的 Promise 一直 pending,后续
catch不触发。原因:执行用户回调未try/catch。修法:handler内try/catch,异常时reject(error)。 - 现象:延迟
resolve后第一段链能跑,后续then不执行。原因:pending 分支push了原始回调,状态变更时未经过handler,新 Promise 未被 settle。修法:push({ onResolved: () => handler(onResolved), onRejected: () => handler(onRejected) })。 - 现象:
p.then().catch(...)拿不到原始reason,或报TypeError。原因:未处理非函数参数,直接调用undefined。修法:成功缺省value => value,失败缺省reason => { throw reason; }。 - 现象:
resolve后reject仍能改变结果。原因:状态变更函数未锁定状态。修法:changeResolved/changeRejected开头判断非pendding直接return。
练习
- 实现
catch(onRejected),要求等价于then(undefined, onRejected),并用延迟reject的用例验证能拿到原始reason。 - 将单对象回调存储改为
#callbackList,编写用例:同一个 Promise 连续注册 3 组成功/失败回调,延迟reject后断言 3 个失败回调都执行。 - 为
handler编写测试用例,覆盖:返回普通值、返回undefined、返回Promise.resolve、返回Promise.reject、回调内抛异常;断言链式调用后的状态和结果。 - 构造一个延迟
resolve的 Promise,在resolve前调用p.then(v => v + 1).then(v => console.log(v)),验证第二个then能拿到第一个then的返回值。 - 给
then增加参数默认值,测试p.then().then(v => console.log(v))与p.then(null, null).catch(r => console.log(r)),要求分别透传原value和原reason。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义Promise-then方法-传入多个回调函数 | 08:59 |
| 2 | 01-自定义Promise-then方法返回新的Promise实例01 | 17:56 |
| 3 | 02-自定义Promise-then方法返回新的Promise实例02 | 25:32 |
| 4 | 自定义Promise-then方法中对上一步代码进一步封装 | 19:49 |
| 5 | 自定义Promise-then方法参数的默认值-实现异常穿透 | 21:39 |
小结
then的回调收集结构必须是列表,单对象只能保留最后一次注册。then必须返回新 Promise,链式调用才有承载对象。- 新 Promise 的状态由本次回调的返回值和异常决定,不由原 Promise 直接决定。
- 回调返回 Promise 时,新 Promise 要接管其状态和结果;返回普通值则直接成功。
- pending 分支保存的回调也必须统一走
handler,否则延迟场景下链式调用会断裂。 - 参数默认值是异常穿透的关键:成功缺省透传值,失败缺省抛出原
reason。 - 状态只允许变更一次,所有用户回调执行都要捕获异常并转为
reject。 - 验证重点应覆盖:多回调、延迟未决、链式返回 Promise、异常穿透。