Skip to content

实现 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() 后改状态”的场景。

易错点 ​

  1. #state 初始值转写稿读作 pendding,标准写法是 pending;判断时前后要一致。
  2. changeRejected 的注释应写“更改为失败”,不要照抄成“成功”。
  3. then() 已决分支用 setTimeout 包回调,是为了模拟 Promise 回调异步执行;不要改成同步直接调用。
  4. 状态改变函数里必须先判断 pending,否则 resolve 后再 reject 会覆盖结果。
  5. 本节只实现“状态改变才执行回调”,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',同样赋默认值,所以效果和不传参数一致。

易错点 ​

  1. 必须用 typeof onResolved !== 'function' 判断,不能只判断 undefined,因为数字、对象等都不是函数。
  2. 失败默认函数必须 throw reason,不能 return reason。
  3. 默认值要放在 then 开头,保证 pending 分支加入 #callbackList 时也已经使用默认函数。
  4. handler 的 try/catch 要包住回调执行和返回值判断,否则回调里的异常不会让新 Promise 失败。
  5. 默认值只影响 then 返回的新 Promise,不会改变原 Promise 的状态;真正的异常穿透最终仍应由 catch 接住处理。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一: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。

练习 ​

  1. 实现 catch(onRejected),要求等价于 then(undefined, onRejected),并用延迟 reject 的用例验证能拿到原始 reason。
  2. 将单对象回调存储改为 #callbackList,编写用例:同一个 Promise 连续注册 3 组成功/失败回调,延迟 reject 后断言 3 个失败回调都执行。
  3. 为 handler 编写测试用例,覆盖:返回普通值、返回 undefined、返回 Promise.resolve、返回 Promise.reject、回调内抛异常;断言链式调用后的状态和结果。
  4. 构造一个延迟 resolve 的 Promise,在 resolve 前调用 p.then(v => v + 1).then(v => console.log(v)),验证第二个 then 能拿到第一个 then 的返回值。
  5. 给 then 增加参数默认值,测试 p.then().then(v => console.log(v)) 与 p.then(null, null).catch(r => console.log(r)),要求分别透传原 value 和原 reason。

原视频对照 ​

#视频时长
1自定义Promise-then方法-传入多个回调函数08:59
201-自定义Promise-then方法返回新的Promise实例0117:56
302-自定义Promise-then方法返回新的Promise实例0225:32
4自定义Promise-then方法中对上一步代码进一步封装19:49
5自定义Promise-then方法参数的默认值-实现异常穿透21:39

小结 ​

  • then 的回调收集结构必须是列表,单对象只能保留最后一次注册。
  • then 必须返回新 Promise,链式调用才有承载对象。
  • 新 Promise 的状态由本次回调的返回值和异常决定,不由原 Promise 直接决定。
  • 回调返回 Promise 时,新 Promise 要接管其状态和结果;返回普通值则直接成功。
  • pending 分支保存的回调也必须统一走 handler,否则延迟场景下链式调用会断裂。
  • 参数默认值是异常穿透的关键:成功缺省透传值,失败缺省抛出原 reason。
  • 状态只允许变更一次,所有用户回调执行都要捕获异常并转为 reject。
  • 验证重点应覆盖:多回调、延迟未决、链式返回 Promise、异常穿透。