手写实现满足 Promise A+ 规范的 Promise:从状态机到 .then 的链式调用

开篇:异步编程的灯塔——Promises/A+ 规范

在现代JavaScript应用开发中,异步操作无处不在:网络请求、文件读写、定时器等等。然而,早期的异步编程模型,如回调函数(callback),常常导致“回调地狱”(Callback Hell),使代码难以阅读、维护和调试。为了解决这一痛点,Promise应运而生,它以一种更优雅、更结构化的方式处理异步操作。

Promise不仅仅是一种编程模式,更是一套被广泛接受的规范——Promises/A+ 规范。这个规范定义了Promise对象的行为、状态转换以及如何与.then方法交互,确保了不同Promise实现之间的互操作性。这意味着,无论是浏览器原生的Promise,还是各种第三方库实现的Promise,只要它们遵循Promises/A+规范,就能够无缝地协同工作。

作为一名编程专家,深入理解并手写实现一个满足Promises/A+规范的Promise,不仅能帮助我们彻底掌握异步编程的核心机制,还能提升我们对JavaScript运行时、事件循环和微任务队列的理解。本次讲座,我们将从Promise最基础的状态机开始,逐步构建起一个完整的Promise实现,直至其最精妙的链式调用机制。

第一章:Promise 的骨架——状态机

Promise的核心在于其严谨的状态机模型。一个Promise对象在其生命周期中,只会处于以下三种状态之一:

  1. Pending (待定):初始状态,既不是成功也不是失败。
  2. Fulfilled (已成功):操作成功完成。
  3. Rejected (已失败):操作失败。

这些状态之间的转换是单向且不可逆的。一旦Promise从Pending状态转换为Fulfilled或Rejected,它就进入了Settled (已落定) 状态,其状态将永远保持不变,并且绑定的值或拒绝原因也不会再改变。

状态转换规则:

  • Pending 状态可以转换为 Fulfilled 状态,伴随一个值 (value)
  • Pending 状态可以转换为 Rejected 状态,伴随一个原因 (reason)
  • Fulfilled 状态不能转换为任何其他状态。
  • Rejected 状态不能转换为任何其他状态。

我们可以用一个表格来清晰地表示这些状态和转换:

状态 描述 转换来源 转换去向 关联数据
pending 初始状态 (无) fulfilled / rejected (无)
fulfilled 操作成功 pending (无) 成功值
rejected 操作失败 pending (无) 失败原因

为了在代码中实现这个状态机,我们需要在MyPromise类的实例中存储当前状态、成功值或失败原因,以及一个用于存储待执行回调的队列。

// 定义Promise的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

// 用于调度微任务的辅助函数
// 在Node.js环境下使用process.nextTick,浏览器环境下优先使用MutationObserver,
// 最后降级为setTimeout(0)。这确保了回调在当前宏任务结束后、下一个宏任务开始前执行。
const _nextTick = (() => {
    if (typeof process === 'object' && typeof process.nextTick === 'function') {
        return process.nextTick;
    } else if (typeof MutationObserver === 'function') {
        let callbacks = [];
        const observer = new MutationObserver(() => {
            const temp = callbacks;
            callbacks = [];
            temp.forEach(cb => cb());
        });
        const textNode = document.createTextNode('');
        observer.observe(textNode, { characterData: true });
        let toggle = false;
        return (cb) => {
            callbacks.push(cb);
            toggle = !toggle;
            textNode.data = toggle ? '1' : '0'; // 触发MutationObserver
        };
    } else {
        return (cb) => setTimeout(cb, 0);
    }
})();

class MyPromise {
    constructor(executor) {
        // 1. 初始化状态
        this._state = PENDING;
        this._value = undefined; // 存储成功的值
        this._reason = undefined; // 存储失败的原因

        // 2. 存储待执行的成功/失败回调函数
        // Promises/A+ 规范要求 onFulfilled/onRejected 必须异步执行,且在一个 Promise 被决议后,
        // 任何后续对 .then 的调用,其相应的回调函数都必须被异步执行。
        // 所以我们使用队列来存储这些回调,在状态改变时异步触发。
        this._fulfilledCallbacks = [];
        this._rejectedCallbacks = [];

        // 3. 构造函数内部,定义 resolve 和 reject 函数
        // 并绑定 this,确保它们在任何地方被调用时都能正确地操作当前Promise实例
        const resolve = (value) => {
            // 使用 _nextTick 确保状态变更和回调执行是异步的
            _nextTick(() => {
                // 只有在 pending 状态时才能进行状态转换
                if (this._state === PENDING) {
                    this._state = FULFILLED;
                    this._value = value;
                    // 遍历执行所有注册的成功回调
                    this._fulfilledCallbacks.forEach(callback => callback());
                }
            });
        };

        const reject = (reason) => {
            _nextTick(() => {
                if (this._state === PENDING) {
                    this._state = REJECTED;
                    this._reason = reason;
                    // 遍历执行所有注册的失败回调
                    this._rejectedCallbacks.forEach(callback => callback());
                }
            });
        };

        // 4. 执行 executor 函数
        // executor 可能会同步抛出错误,需要捕获并调用 reject
        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    // ... 后续将实现 .then 方法
}

这段代码构建了MyPromise的基础骨架。_state_value_reason是Promise的内部状态,_fulfilledCallbacks_rejectedCallbacks是两个队列,用于存储通过.then方法注册的回调函数。resolvereject函数负责改变Promise的状态,并异步触发相应的回调。executor函数是Promise的执行器,它会被立即同步执行,并接收resolvereject作为参数。任何在executor中抛出的异常都应该导致Promise被拒绝。

第二章:Promise 的生命周期——构造函数与核心解析逻辑

在上一章的基础上,我们已经看到了MyPromise构造函数的初步实现。它接收一个executor函数,这个函数会被立即执行,并传入两个参数:resolverejectexecutor的职责是启动异步操作,并在操作成功时调用resolve,失败时调用reject

resolvereject的职责:

  1. 改变Promise状态:将_statePENDING更改为FULFILLEDREJECTED
  2. 存储结果:将成功值存储到_value,或将失败原因存储到_reason
  3. 触发回调:遍历并执行_fulfilledCallbacks_rejectedCallbacks队列中的所有回调函数。
  4. 单次决议原则:一个Promise只能被决议(resolve或reject)一次。一旦状态从PENDING变为FULFILLEDREJECTED,后续对resolvereject的调用都将被忽略。
  5. 异步执行回调:Promises/A+ 规范明确要求onFulfilledonRejected回调必须作为微任务异步执行。这是为了确保一致的行为,避免同步执行可能导致的竞态条件或意外的副作用。我们通过_nextTick辅助函数来调度这些微任务。

让我们仔细审视resolvereject函数的实现,它们是Promise生命周期的关键:

// ... (MyPromise class definition starts)

class MyPromise {
    // ... (constructor and _nextTick defined as before)

    constructor(executor) {
        this._state = PENDING;
        this._value = undefined;
        this._reason = undefined;
        this._fulfilledCallbacks = [];
        this._rejectedCallbacks = [];

        // 内部的 resolve 方法,用于将 Promise 状态从 pending 变为 fulfilled
        const _resolve = (value) => {
            // 确保状态改变和回调执行是异步的 (Promises/A+ 2.2.4)
            _nextTick(() => {
                // 只能从 pending 状态转换为 fulfilled (Promises/A+ 2.1.1.1)
                if (this._state === PENDING) {
                    this._state = FULFILLED;
                    this._value = value;
                    // 执行所有已注册的 onFulfilled 回调 (Promises/A+ 2.2.6)
                    this._fulfilledCallbacks.forEach(callback => callback());
                }
            });
        };

        // 内部的 reject 方法,用于将 Promise 状态从 pending 变为 rejected
        const _reject = (reason) => {
            // 确保状态改变和回调执行是异步的 (Promises/A+ 2.2.4)
            _nextTick(() => {
                // 只能从 pending 状态转换为 rejected (Promises/A+ 2.1.1.2)
                if (this._state === PENDING) {
                    this._state = REJECTED;
                    this._reason = reason;
                    // 执行所有已注册的 onRejected 回调 (Promises/A+ 2.2.6)
                    this._rejectedCallbacks.forEach(callback => callback());
                }
            });
        };

        // 执行 executor 函数,捕获可能抛出的同步错误 (Promises/A+ 2.3.3.3.4)
        try {
            executor(_resolve, _reject);
        } catch (e) {
            _reject(e); // 如果 executor 抛出错误,则拒绝 Promise
        }
    }
    // ... (will implement .then method next)
}

这里我们把resolvereject函数命名为_resolve_reject,以区分它们与Promises/A+规范中的Promise Resolution Procedure,并强调它们是Promise实例内部用来改变状态的核心逻辑。

关于_nextTick的重要性:

_nextTick函数的使用是严格遵循Promises/A+规范的关键一环。规范要求onFulfilledonRejected回调必须在当前执行栈清空后,以异步的方式执行。这通常通过将它们放入JavaScript的微任务队列来实现。

  • process.nextTick (Node.js):在当前事件循环的下一个“tick”中执行回调,优先级高于setTimeout(0)
  • MutationObserver (浏览器):利用DOM变化作为触发器,创建微任务。比setTimeout(0)更接近微任务的行为。
  • setTimeout(0) (降级方案):将回调放入宏任务队列,优先级最低。在没有微任务机制的环境下,这是最接近异步执行的方式。

通过_nextTick,我们确保了resolvereject即使被同步调用,其效果(状态改变和回调执行)也是在后续的微任务中完成的,从而避免了“同步决议”可能导致的问题,并保证了then方法总是返回一个新的Promise,且其回调总是异步执行。

第三章:连接未来的桥梁——.then 方法的实现

.then方法是Promise链式调用的核心,也是与外界交互的主要接口。它允许我们注册在Promise成功或失败时要执行的回调函数。Promises/A+ 规范对.then方法的行为有着严格的定义。

.then方法的规范要求:

  1. 签名promise.then(onFulfilled, onRejected)
    • onFulfilled:可选参数,类型必须是函数。如果不是函数,则必须被忽略。
    • onRejected:可选参数,类型必须是函数。如果不是函数,则必须被忽略。
  2. 返回值.then方法必须返回一个新的Promise对象,这正是链式调用的基础。
  3. 回调的异步执行onFulfilledonRejected必须作为微任务异步执行。
  4. 回调的执行次数onFulfilledonRejected只能被调用一次。
  5. 值传递
    • 如果promisefulfilledonFulfilled会被调用,其第一个参数是promise的成功值。
    • 如果promiserejectedonRejected会被调用,其第一个参数是promise的拒绝原因。
  6. 错误处理:如果onFulfilledonRejected回调自身抛出异常,那么.then返回的新Promise必须被拒绝,拒绝原因为该异常。
  7. 透传
    • 如果onFulfilled不是函数,且promisefulfilledthen返回的新Promise必须以promise的成功值fulfilled
    • 如果onRejected不是函数,且promiserejectedthen返回的新Promise必须以promise的拒绝原因rejected

现在,让我们来实现MyPromise.then方法:

class MyPromise {
    // ... (constructor and other methods as defined before)

    then(onFulfilled, onRejected) {
        // 1. 规范 2.2.1: onFulfilled 和 onRejected 必须是函数。
        // 如果不是函数,我们需要创建一个“透传”函数,确保值或错误能传递下去。
        // 规范 2.2.7.3: 如果 onFulfilled 不是函数,promise2 必须使用 promise1 的值 fulfill。
        // 规范 2.2.7.4: 如果 onRejected 不是函数,promise2 必须使用 promise1 的原因 reject。
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

        // 2. 规范 2.2.7: .then 必须返回一个 Promise。
        const promise2 = new MyPromise((resolve, reject) => {
            // 3. 根据当前 Promise (this) 的状态来处理回调
            if (this._state === FULFILLED) {
                // 3.1 如果当前 Promise 已经 fulfilled,异步执行 onFulfilled
                _nextTick(() => {
                    try {
                        // 3.1.1 规范 2.2.5, 2.2.7.1: onFulfilled 必须被调用,且以 value 为参数。
                        // 3.1.2 规范 2.2.7.1: onFulfilled 的返回值 x 决定 promise2 的状态。
                        const x = onFulfilled(this._value);
                        // 调用 Promise Resolution Procedure 来处理 x
                        _resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        // 3.1.3 规范 2.2.7.2: 如果 onFulfilled 抛出异常 e,promise2 必须以 e 为原因 rejected。
                        reject(e);
                    }
                });
            } else if (this._state === REJECTED) {
                // 3.2 如果当前 Promise 已经 rejected,异步执行 onRejected
                _nextTick(() => {
                    try {
                        // 3.2.1 规范 2.2.5, 2.2.7.1: onRejected 必须被调用,且以 reason 为参数。
                        // 3.2.2 规范 2.2.7.1: onRejected 的返回值 x 决定 promise2 的状态。
                        const x = onRejected(this._reason);
                        // 调用 Promise Resolution Procedure 来处理 x
                        _resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        // 3.2.3 规范 2.2.7.2: 如果 onRejected 抛出异常 e,promise2 必须以 e 为原因 rejected。
                        reject(e);
                    }
                });
            } else if (this._state === PENDING) {
                // 3.3 如果当前 Promise 仍在 pending 状态,将回调函数保存起来
                // 当 Promise 状态改变时,会遍历执行这些回调
                this._fulfilledCallbacks.push(() => {
                    _nextTick(() => { // 再次确保异步
                        try {
                            const x = onFulfilled(this._value);
                            _resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
                this._rejectedCallbacks.push(() => {
                    _nextTick(() => { // 再次确保异步
                        try {
                            const x = onRejected(this._reason);
                            _resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            }
        });

        return promise2; // 返回新的 Promise,实现链式调用
    }
}

then方法中,我们创建了一个新的MyPromise实例promise2并将其返回。这个promise2的状态将由onFulfilledonRejected回调函数的执行结果来决定。

特别要注意的是,无论当前Promise处于何种状态,onFulfilledonRejected回调的执行以及promise2的决议,都必须通过_resolvePromise函数来处理。这是Promises/A+规范中最复杂但也是最核心的部分——Promise决议过程

第四章:链式调用的奥秘——Promise 决议过程 (Promise Resolution Procedure)

Promise Resolution Procedure,即Promise决议过程,是Promises/A+规范中最核心、最精妙的部分,也是实现.then链式调用和互操作性的关键。它的职责是根据onFulfilledonRejected回调函数的返回值x,来决定由.then方法返回的新Promise (promise2) 的最终状态。

规范 2.3 详细描述了这个过程,我们将其封装在一个名为_resolvePromise(promise2, x, resolve, reject)的辅助函数中。

参数说明:

  • promise2:由.then方法返回的新Promise。
  • xonFulfilledonRejected回调函数的返回值。
  • resolvepromise2resolve函数。
  • rejectpromise2reject函数。

Promise决议过程 _resolvePromise 的详细逻辑:

| 规范点 | 条件 (x 的类型或特征) | 处理方式 My MyPromise {
constructor(executor) {
this._state = PENDING;
this._value = undefined;
this._reason = undefined;
this._fulfilledCallbacks = [];
this._rejectedCallbacks = [];

    const _resolve = (value) => {
        _nextTick(() => {
            if (this._state === PENDING) {
                this._state = FULFILLED;
                this._value = value;
                this._fulfilledCallbacks.forEach(callback => callback());
            }
        });
    };

    const _reject = (reason) => {
        _nextTick(() => {
            if (this._state === PENDING) {
                this._state = REJECTED;
                this._reason = reason;
                this._rejectedCallbacks.forEach(callback => callback());
            }
        });
    };

    try {
        executor(_resolve, _reject);
    } catch (e) {
        _reject(e);
    }
}

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

    const promise2 = new MyPromise((resolve, reject) => {
        const handleCallback = (callback, valueOrReason) => {
            _nextTick(() => {
                try {
                    const x = callback(valueOrReason);
                    _resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        };

        if (this._state === FULFILLED) {
            handleCallback(onFulfilled, this._value);
        } else if (this._state === REJECTED) {
            handleCallback(onRejected, this._reason);
        } else if (this._state === PENDING) {
            this._fulfilledCallbacks.push(() => handleCallback(onFulfilled, this._value));
            this._rejectedCallbacks.push(() => handleCallback(onRejected, this._reason));
        }
    });

    return promise2;
}

}

/**

  • Promise Resolution Procedure (规范 2.3)
  • 用于处理 onFulfilled 或 onRejected 的返回值 x,以此决定 promise2 的状态。
  • @param {MyPromise} promise2 – .then 方法返回的新 Promise
  • @param {*} x – onFulfilled 或 onRejected 的返回值
  • @param {Function} resolve – promise2 的 resolve 函数
  • @param {Function} reject – promise2 的 reject 函数
    */
    function _resolvePromise(promise2, x, resolve, reject) {
    // 2.3.1: 如果 promise2 和 x 指向同一个对象,则以 TypeError 拒绝 promise2。
    // 这是为了防止循环引用,例如 return this;
    if (promise2 === x) {
    return reject(new TypeError(‘Chaining cycle detected for promise #’));
    }

    // 2.3.2: 如果 x 是一个 Promise,则采用它的状态。
    // 意思是如果 x 也是一个 Promise,promise2 将会等待 x 的状态。
    if (x instanceof MyPromise) {
    // 2.3.2.1: 如果 x 是 pending 状态,promise2 必须保持 pending 直到 x 变为 fulfilled 或 rejected。
    // 2.3.2.2: 如果 x 是 fulfilled 状态,则以 x 的值 fulfill promise2。
    // 2.3.2.3: 如果 x 是 rejected 状态,则以 x 的原因 reject promise2。
    x.then(resolve, reject); // 这里直接将 promise2 的 resolve/reject 传递给 x.then
    return;
    }

    // 2.3.3: 如果 x 是一个对象或函数 (即可能是 thenable)。
    // thenable 是指任何拥有 then 方法的对象或函数。
    if (x && (typeof x === ‘object’ || typeof x === ‘function’)) {
    let called = false; // 确保 thenable 的 then 方法只被调用一次

    try {
        // 2.3.3.1: 令 then 为 x.then。
        // 2.3.3.2: 如果取 x.then 抛出异常 e,则以 e 拒绝 promise2。
        const then = x.then;
    
        // 2.3.3.3: 如果 then 是一个函数,则以 x 作为 this 调用它,
        // 并传入两个回调函数:`resolvePromise` 和 `rejectPromise`。
        if (typeof then === 'function') {
            then.call(
                x,
                // 2.3.3.3.1: 如果 `resolvePromise` 被调用,参数为 `y`,则运行 `[[Resolve]](promise2, y)`。
                (y) => {
                    if (called) return; // 确保只被调用一次 (Promises/A+ 2.3.3.3.3)
                    called = true;
                    _resolvePromise(promise2, y, resolve, reject); // 递归解析 y
                },
                // 2.3.3.3.2: 如果 `rejectPromise` 被调用,参数为 `r`,则以 `r` 拒绝 promise2。
                (r) => {
                    if (called) return; // 确保只被调用一次 (Promises/A+ 2.3.3.3.3)
                    called = true;
                    reject(r);
                }
            );
            return; // 处理完毕,返回
        }
    } catch (e) {
        // 2.3.3.3.4: 如果调用 then 抛出异常 e,
        // 如果 `resolvePromise` 或 `rejectPromise` 已经被调用,则忽略该异常。
        // 否则,以 e 拒绝 promise2。
        if (called) return; // 如果已经调用过 resolvePromise/rejectPromise,则忽略后续的异常
        reject(e);
        return;
    }

    }

    // 2.3.4: 如果 x 既不是对象也不是函数,则以 x fulfill promise2。
    resolve(x);
    }

_resolvePromise 函数的核心逻辑分析:

  1. 循环引用检测 (2.3.1):如果promise2x是同一个对象,这意味着在onFulfilledonRejected中返回了promise2自身,这会形成一个无限循环。规范要求此时必须以TypeError拒绝promise2

  2. x 是一个 Promise (2.3.2)

    • 如果x本身也是一个MyPromise实例,那么promise2的状态将完全跟随x的状态。
    • 这通过x.then(resolve, reject)实现:当x成功时,promise2也成功;当x失败时,promise2也失败。
    • 这是实现Promise链式调用中,返回Promise的关键。
  3. x 是一个对象或函数 (可能是 Thenable) (2.3.3)

    • 获取then方法 (2.3.3.1 & 2.3.3.2):尝试获取x.then属性。如果获取过程中抛出异常,则promise2被拒绝。
    • then是函数 (2.3.3.3):如果x.then是一个函数,那么x被视为一个“thenable”对象。
      • 我们必须以x作为this上下文调用x.then
      • 传入两个回调函数:resolvePromiserejectPromise
      • resolvePromise(y):如果thenable内部调用了resolvePromise,且参数为y,那么我们需要递归地调用_resolvePromise(promise2, y, resolve, reject)来继续解析y。这允许thenable返回另一个thenable或Promise,实现更深层次的链式解析。
      • rejectPromise(r):如果thenable内部调用了rejectPromise,且参数为r,那么promise2被以r拒绝。
      • called标志 (2.3.3.3.3)thenablethen方法可能会多次调用resolvePromiserejectPromise,或者同时调用两者。规范要求只接受第一次调用。called标志就是为此目的。
      • 异常处理 (2.3.3.3.4):如果在调用then函数过程中或其内部抛出异常,如果此时resolvePromiserejectPromise已经被调用过,则忽略该异常;否则,promise2被以该异常拒绝。
  4. x 是普通值 (非对象、非函数) (2.3.4)

    • 如果x不是对象也不是函数(即普通JavaScript值,如字符串、数字、布尔值、nullundefined),那么promise2直接以x为值被fulfilled

这个_resolvePromise函数是Promises/A+规范的精髓。它通过递归和状态管理,巧妙地将任意类型的返回值x,统一地映射到Promise的状态转换上,从而实现了Promise之间的无缝衔接和链式调用。

第五章:异步与微任务——确保正确时序

在Promise的实现中,异步执行是一个核心原则,而微任务队列则是实现这一原则的关键机制。

为什么需要异步执行?

Promises/A+规范 2.2.4 规定:onFulfilledonRejected必须异步执行。具体来说,它们必须在then方法被调用的那个执行栈完成之后,且在新的宏任务开始之前执行。这背后的原因有:

  1. 避免竞态条件:如果回调同步执行,那么在某些情况下,Promise可能会在executor函数中同步完成,导致then方法被调用时,Promise可能已经处于fulfilledrejected状态。同步执行回调可能会在then方法返回新的Promise之前改变状态,从而引入难以预测的行为。异步执行确保了then方法总能返回一个pending状态的Promise(即使只是短暂的),再由微任务机制在稍后处理回调。
  2. 保证一致的行为:无论Promise是同步决议(例如Promise.resolve(value))还是异步决议(例如网络请求),其.then回调的执行时机都应该保持一致。异步执行使得Promise的行为更加可预测。
  3. 避免栈溢出:在深层Promise链中,如果回调同步执行,可能会导致递归调用过深,从而引发栈溢出。将回调推入微任务队列,可以有效避免这个问题,因为每次回调都在一个新的(微)任务中执行,不会增加当前调用栈的深度。

微任务队列 (Microtask Queue) 与宏任务队列 (Macrotask Queue)

JavaScript运行时有一个事件循环机制,它管理着任务的执行。任务可以分为两类:

  • 宏任务 (Macrotasks):如setTimeout, setInterval, setImmediate (Node.js), I/O, UI渲染等。
  • 微任务 (Microtasks):如Promise的回调 (.then, .catch, .finally), MutationObserver的回调, process.nextTick (Node.js)等。

事件循环在一个宏任务执行完成后,会检查并清空所有可用的微任务,然后才进入下一个宏任务。这意味着微任务的优先级高于宏任务。

我们在代码中已经引入了_nextTick辅助函数,它就是用来将回调函数推入微任务队列的机制:

// ... (MyPromise class definition starts)

// 用于调度微任务的辅助函数
// 在Node.js环境下使用process.nextTick,浏览器环境下优先使用MutationObserver,
// 最后降级为setTimeout(0)。这确保了回调在当前宏任务结束后、下一个宏任务开始前执行。
const _nextTick = (() => {
    if (typeof process === 'object' && typeof process.nextTick === 'function') {
        return process.nextTick; // Node.js 环境
    } else if (typeof MutationObserver === 'function') {
        let callbacks = [];
        const observer = new MutationObserver(() => {
            // 当 MutationObserver 被触发时,执行所有收集到的回调
            const temp = callbacks;
            callbacks = []; // 清空队列,准备下一轮
            temp.forEach(cb => cb());
        });
        const textNode = document.createTextNode('');
        observer.observe(textNode, { characterData: true }); // 观察一个文本节点的 characterData 变化
        let toggle = false;
        return (cb) => {
            callbacks.push(cb); // 将回调函数添加到队列
            toggle = !toggle;
            textNode.data = toggle ? '1' : '0'; // 改变文本节点数据,触发 MutationObserver
        };
    } else {
        return (cb) => setTimeout(cb, 0); // 浏览器降级方案,作为宏任务执行
    }
})();

// ... (MyPromise class definition continues)

_nextTick在Promise实现中的应用:

  1. _resolve_reject方法内部

    const _resolve = (value) => {
        _nextTick(() => { // 确保状态变更和回调执行是异步的
            if (this._state === PENDING) {
                this._state = FULFILLED;
                this._value = value;
                this._fulfilledCallbacks.forEach(callback => callback());
            }
        });
    };

    这里_nextTick包裹了状态修改和回调触发的逻辑。这意味着即使executor同步调用了_resolve,Promise的状态改变和_fulfilledCallbacks的执行也会被推迟到当前宏任务结束后。

  2. then方法内部

    • 当Promise处于FULFILLEDREJECTED状态时,onFulfilledonRejected回调的执行也被包裹在_nextTick中。
      if (this._state === FULFILLED) {
          handleCallback(onFulfilled, this._value); // handleCallback 内部也有 _nextTick
      }
      // ...
      const handleCallback = (callback, valueOrReason) => {
          _nextTick(() => { // 确保回调执行异步
              try {
                  const x = callback(valueOrReason);
                  _resolvePromise(promise2, x, resolve, reject);
              } catch (e) {
                  reject(e);
              }
          });
      };
    • 当Promise处于PENDING状态时,回调被注册到_fulfilledCallbacks_rejectedCallbacks队列中。当Promise最终决议时,这些回调会被取出并执行,而执行它们的代码本身已经在_resolve_reject中被_nextTick包裹,因此自然也是异步的。

通过这种方式,我们严格遵循了Promises/A+规范关于异步执行的要求,确保了Promise行为的稳定性和一致性。理解并正确运用微任务队列,是掌握Promise高级特性的关键。

第六章:完善与优化——边缘情况处理

一个健壮的Promise实现需要考虑各种边缘情况和细节,以确保其完全符合Promises/A+规范。

1. onFulfilled/onRejected 参数的默认行为 (透传)

规范 2.2.7.3 和 2.2.7.4 规定:

  • 如果onFulfilled不是函数,且promise1fulfilled,那么promise2(由promise1.then()返回的)必须以promise1的成功值fulfilled
  • 如果onRejected不是函数,且promise1rejected,那么promise2必须以promise1的拒绝原因rejected

这意味着,如果我们在.then中省略了某个回调,它应该表现为“透传”:成功值继续传递给下一个onFulfilled,拒绝原因继续传递给下一个onRejected

在我们的then方法中,已经处理了这一点:

    then(onFulfilled, onRejected) {
        // 如果 onFulfilled 不是函数,创建一个默认函数,直接返回传入的值
        // 使得 promise2 能够继续以 promise1 的值 fulfilled
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        // 如果 onRejected 不是函数,创建一个默认函数,抛出传入的原因
        // 使得 promise2 能够继续以 promise1 的原因 rejected (因为抛出的错误会被 catch 捕获并 reject promise2)
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

        // ...
    }
  • 对于onFulfilled的默认值value => value,当它被调用时,返回的就是原始的成功值。_resolvePromise会收到这个值,并用它来resolve promise2
  • 对于onRejected的默认值reason => { throw reason; },当它被调用时会抛出原始的拒绝原因。.then方法中的try...catch会捕获这个错误,并用它来reject promise2。这巧妙地实现了拒绝原因的透传。

2. Promise.resolvePromise.reject 静态方法

这两个静态方法是创建已决议Promise的便捷方式。

  • Promise.resolve(value):返回一个以value成功决议的Promise。如果value本身是一个Promise,则返回该Promise。如果value是一个thenable,则会尝试解析thenable。
  • Promise.reject(reason):返回一个以reason拒绝决议的Promise。
// 在 MyPromise 类外部或者作为静态方法实现
MyPromise.resolve = function(value) {
    // 如果 value 是 MyPromise 实例,直接返回
    if (value instanceof MyPromise) {
        return value;
    }
    // 否则,返回一个新的 MyPromise,并立即 resolve
    return new MyPromise(resolve => {
        resolve(value); // 这里的 resolve 会触发 _resolvePromise 逻辑
    });
};

MyPromise.reject = function(reason) {
    // 返回一个新的 MyPromise,并立即 reject
    return new MyPromise((_, reject) => {
        reject(reason);
    });
};

注意MyPromise.resolve的实现中,它会创建一个新的Promise,并调用resolve(value)。这个resolve是我们MyPromise构造函数中定义的内部_resolve函数。当value是thenable时,_resolve内部会调用_resolvePromise,从而正确处理thenable的解析。

3. 错误处理与冒泡

Promise的错误处理机制是一个非常强大的特性。当一个Promise被拒绝时,如果没有onRejected回调来处理它,错误会沿着Promise链向下冒泡,直到遇到一个onRejected回调或者到达链的末端。

  • executor中的错误:在MyPromise的构造函数中,我们使用try...catch来捕获executor函数中可能抛出的同步错误,并将其转换为Promise的拒绝原因。
  • onFulfilled/onRejected回调中的错误:在.then方法中,onFulfilledonRejected回调的执行也包裹在try...catch中。如果回调内部抛出异常,这个异常会成为promise2的拒绝原因。
  • 未捕获的错误:如果Promise链的最后没有onRejected回调来处理错误,或者最后一个onRejected回调本身又抛出了错误,那么这个错误通常会成为“未捕获的Promise拒绝”,在浏览器中会触发unhandledrejection事件,在Node.js中会触发unhandledRejection进程事件。

一个完整的MyPromise类:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

const _nextTick = (() => {
    if (typeof process === 'object' && typeof process.nextTick === 'function') {
        return process.nextTick;
    } else if (typeof MutationObserver === 'function') {
        let callbacks = [];
        const observer = new MutationObserver(() => {
            const temp = callbacks;
            callbacks = [];
            temp.forEach(cb => cb());
        });
        const textNode = document.createTextNode('');
        observer.observe(textNode, { characterData: true });
        let toggle = false;
        return (cb) => {
            callbacks.push(cb);
            toggle = !toggle;
            textNode.data = toggle ? '1' : '0';
        };
    } else {
        return (cb) => setTimeout(cb, 0);
    }
})();

class MyPromise {
    constructor(executor) {
        this._state = PENDING;
        this._value = undefined;
        this._reason = undefined;
        this._fulfilledCallbacks = [];
        this._rejectedCallbacks = [];

        const _resolve = (value) => {
            _nextTick(() => {
                if (this._state === PENDING) {
                    this._state = FULFILLED;
                    this._value = value;
                    this._fulfilledCallbacks.forEach(callback => callback());
                }
            });
        };

        const _reject = (reason) => {
            _nextTick(() => {
                if (this._state === PENDING) {
                    this._state = REJECTED;
                    this._reason = reason;
                    this._rejectedCallbacks.forEach(callback => callback());
                }
            });
        };

        try {
            executor(_resolve, _reject);
        } catch (e) {
            _reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

        const promise2 = new MyPromise((resolve, reject) => {
            const handleCallback = (callback, valueOrReason) => {
                _nextTick(() => {
                    try {
                        const x = callback(valueOrReason);
                        _resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            };

            if (this._state === FULFILLED) {
                handleCallback(onFulfilled, this._value);
            } else if (this._state === REJECTED) {
                handleCallback(onRejected, this._reason);
            } else if (this._state === PENDING) {
                this._fulfilledCallbacks.push(() => handleCallback(onFulfilled, this._value));
                this._rejectedCallbacks.push(() => handleCallback(onRejected, this._reason));
            }
        });

        return promise2;
    }

    // 实现 .catch 方法,实际上就是 .then(null, onRejected) 的语法糖
    catch(onRejected) {
        return this.then(null, onRejected);
    }

    // 实现 .finally 方法 (ES2018),无论成功或失败都会执行,且不改变原 Promise 的状态和值
    finally(callback) {
        return this.then(
            value => {
                return MyPromise.resolve(callback()).then(() => value);
            },
            reason => {
                return MyPromise.resolve(callback()).then(() => { throw reason; });
            }
        );
    }

    // 静态方法
    static resolve(value) {
        if (value instanceof MyPromise) {
            return value;
        }
        return new MyPromise(resolve => {
            resolve(value); // 这里的 resolve 会触发 _resolvePromise 逻辑
        });
    }

    static reject(reason) {
        return new MyPromise((_, reject) => {
            reject(reason);
        });
    }
}

function _resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'));
    }

    if (x instanceof MyPromise) {
        x.then(resolve, reject);
        return;
    }

    if (x && (typeof x === 'object' || typeof x === 'function')) {
        let called = false;

        try {
            const then = x.then;

            if (typeof then === 'function') {
                then.call(
                    x,
                    (y) => {
                        if (called) return;
                        called = true;
                        _resolvePromise(promise2, y, resolve, reject);
                    },
                    (r) => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                );
                return;
            }
        } catch (e) {
            if (called) return;
            reject(e);
            return;
        }
    }

    resolve(x);
}

至此,我们的MyPromise实现已经相当完善,不仅满足了Promises/A+的核心规范,还增加了常用的catchfinally方法,以及Promise.resolvePromise.reject静态方法。

第七章:测试与验证——Promises/A+ 规范的严格要求

仅仅实现了代码是不够的,我们还需要确保它完全符合Promises/A+规范的所有细则。Promises/A+规范提供了一个官方的测试套件 promises-aplus-tests,专门用于验证Promise实现的合规性。

使用 promises-aplus-tests

  1. 安装测试工具
    npm install -g promises-aplus-tests
  2. 创建适配器promises-aplus-tests工具需要一个适配器来与你的Promise实现进行交互。这个适配器需要提供一个deferred对象,包含promiseresolvereject方法。

    // adapters.js
    const MyPromise = require('./myPromise'); // 假设你的 MyPromise 在 myPromise.js 中
    
    module.exports = {
        deferred: function () {
            let resolve, reject;
            const promise = new MyPromise((_resolve, _reject) => {
                resolve = _resolve;
                reject = _reject;
            });
            return {
                promise: promise,
                resolve: resolve,
                reject: reject
            };
        }
    };
  3. 运行测试
    promises-aplus-tests adapters.js

    如果你的实现通过了所有测试(通常是几百个测试用例),那么恭喜你,你的Promise实现是完全符合Promises/A+规范的。如果有些测试失败,测试报告会详细指出失败的规范点,帮助你定位和修复问题。

通过严格的测试,我们可以确保我们的MyPromise不仅在表面上工作,而且在所有边缘情况下都表现出符合规范的预期行为。

尾声:展望未来,掌握核心

至此,我们已经完整地手写实现了一个满足Promises/A+规范的Promise。从理解状态机到.then方法的链式调用,再到精妙的Promise决议过程和微任务调度,我们一步步揭开了Promise内部运作的神秘面纱。

这个过程不仅加深了我们对Promise本身的理解,更重要的是,它锻炼了我们对JavaScript异步编程、事件循环、错误处理以及规范细节的把控能力。掌握这些核心原理,将使我们能够更自信地使用Promise,编写出更健壮、更可维护的异步代码,并为未来学习async/await等更高层级的异步抽象打下坚实的基础。理解Promise,就是理解现代JavaScript异步编程的基石。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注