手写Promise教程:逐步实现现代 JavaScript 异步处理

279 阅读10分钟

在现代 JavaScript 开发中,Promise 是处理异步操作的重要工具。通过学习手写一个符合 Promise/A+ 规范Promise 实现,不仅可以帮助我们更好地理解异步编程的核心概念,还能增强我们解决复杂问题的能力。接下来,我们将从最基本的步骤开始,逐步实现一个简化版的 Promise,并详细解释每一个步骤。

Promise 的基本结构

首先,我们创建一个基础的 Promise 类。这个类的构造函数接受一个 executor 函数作为参数,并立即执行该函数。

class Promise {
    constructor(executor) {
        // 成功时的函数
        let resolve = () => { };
        // 失败时的函数
        let reject = () => { };
        // 立即执行传入的 executor 函数
        executor(resolve, reject);
    }
}

这种基本结构定义了一个 Promise 类,该类在实例化时立即执行 executor 函数,并传入两个参数:resolvereject

Step 1: 增加状态和结果处理

接下来,我们要处理 Promise 的三种状态,以及成功和失败时的结果。

初始化状态、值和原因

我们需要为这个 Promise 初始化状态(pending)、成功的值(value)和失败的原因(reason)。

class Promise {
    constructor(executor) {
        this.state = 'pending'; // 初始状态为 pending 等待态
        this.value = undefined; // 成功时的值
        this.reason = undefined; // 失败时的原因

        // 成功时的函数
        let resolve = (value) => { 
            if (this.state === 'pending') { // 只能从 pending 变为 fulfilled
                this.state = 'fulfilled';
                this.value = value;
            }
        };

        // 失败时的函数
        let reject = (reason) => { 
            if (this.state === 'pending') { // 只能从 pending 变为 rejected
                this.state = 'rejected';
                this.reason = reason;
            }
        };

        // 立即执行传入的 executor 函数
        try {
            executor(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
}

在这里,我们做了以下几件事:

  1. 初始化 state 属性为 pending,以表示初始状态。
  2. 初始化 valuereason 属性分别用于存储成功的值和失败的原因。
  3. 定义 resolvereject 函数用于改变 Promise 的状态和结果。
  4. 立即执行 executor 函数,并在执行过程中捕获任何错误,若有错误则调用 reject 函数。

Step 2: 增加状态变更的回调处理——解决异步场景

在现代应用中,异步操作(如 setTimeout)是非常常见的。因此,我们需要确保在 resolve 或 reject 被异步调用时,能够正确地处理这些情况。

当 Promise 处于 pending 状态时,应该存储所有的回调函数(onFulfilledonRejected),等状态变为 fulfilledrejected 时,再依次执行这些回调函数。

添加回调数组

我们需要为 Promise 添加两个数组,用来存储成功和失败的回调函数。这些回调函数会在 resolvereject 被调用时执行:

class Promise {
    constructor(executor) {
        this.state = 'pending'; // 初始状态
        this.value = undefined; // 成功的值
        this.reason = undefined; // 失败的原因
        this.onResolvedCallbacks = []; // 存储成功的回调
        this.onRejectedCallbacks = []; // 存储失败的回调

        const resolve = (value) => {
            if (this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                this.onResolvedCallbacks.forEach(fn => fn()); // 执行所有成功的回调
            }
        };

        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn()); // 执行所有失败的回调
            }
        };

        try {
            executor(resolve, reject); // 立即执行 executor
        } catch (err) {
            reject(err); // 如果执行器抛错,直接执行 reject
        }
    }
}

then 方法

为了在 Promise 状态变更时执行相应的回调函数,我们需要在 then 方法中处理这些回调。then 方法接收两个参数:onFulfilledonRejected。如果 Promise 状态已经变为 fulfilledrejected,则立即执行相应的回调。如果 Promise 仍处于 pending 状态,则将回调函数存储起来。

class Promise {
    constructor(executor) {
        // 构造函数代码略...
    }

    then(onFulfilled, onRejected) {
        if (this.state === 'fulfilled') {
            onFulfilled(this.value);  // 成功态直接执行回调,并传入成功结果
        } else if (this.state === 'rejected') {
            onRejected(this.reason);  // 失败态直接执行回调,并传入失败原因
        } else if (this.state === 'pending') {
            // pending 状态时,将 onFulfilled 和 onRejected 存入对应的回调数组
            this.onResolvedCallbacks.push(() => {
                onFulfilled(this.value);
            });
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            });
        }
    }
}

Step 3: 实现链式调用与确保异步执行

在现代应用中,Promise 的链式调用和异步回调是至关重要的。为了实现 .then().then() 这样的链式调用,我们需要对 then 方法进行改进,使其能够返回一个新的 Promise。同时,为了确保回调函数是异步执行的,我们使用 queueMicrotask

为什么使用 queueMicrotask

根据 Promise 规范,onFulfilledonRejected 回调必须在当前的执行栈完成之后异步执行。使用 queueMicrotask 可以确保回调函数在当前事件循环结束后立即执行,而不是等待下一个事件循环。这对于优化性能和确保正确的执行顺序非常重要,而不是使用 setTimeout

使用 setTimeout 也可以实现异步执行,但它会在所有的宏任务完成后才会执行,因此会有较大的延时。而 queueMicrotask 更加高效,因为它会在当前事件循环结束后立即执行。

class Promise {
    constructor(executor) {
        // 构造函数代码略
    }

    then(onFulfilled, onRejected) {
        const promise2 = new Promise((resolve, reject) => {
            if (this.state === 'fulfilled') {
                queueMicrotask(() => {
                    try {
                        const x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            } else if (this.state === 'rejected') {
                queueMicrotask(() => {
                    try {
                        const x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            } else if (this.state === 'pending') {
                this.onResolvedCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    });
                });
                this.onRejectedCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    });
                });
            }
        });

        return promise2;
    }
}

为什么 then 方法需要类型检查与默认处理

在 Promise 的 then 方法中,我们添加了关于 onFulfilled 和 onRejected 的类型检查和默认处理:

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

这些代码的作用是确保 then 方法中的回调函数是有效的函数,并为可能的空值赋予默认值。

  1. onFulfilled:

    • 检查传入的 onFulfilled 参数是否为函数。
    • 如果 onFulfilled 是有效的函数,则保持原样。
    • 如果 onFulfilled 不是函数(例如 undefined),则替换为一个默认函数,该函数简单地返回传入的值:
    value => value
    

    这种默认处理方式使得后续的 then 方法能够继续操作,即使没有提供 onFulfilled 回调函数。

    promise.then(null, onRejected); // 等价于
    promise.then(value => value, onRejected);
    
  2. onRejected:

    • 检查传入的 onRejected 参数是否为函数。
    • 如果 onRejected 是有效的函数,则保持原样。
    • 如果 onRejected 不是函数(例如 undefined),则替换为一个默认函数,该函数简单地抛出传入的原因:
    reason => { throw reason }
    

    这种默认处理方式确保如果没有提供 onRejected 回调函数,拒绝的原因能够向后传递,进而被后续的 catch 块处理。

为什么 then 方法必须返回一个新的 Promise

then 方法必须返回一个新的 Promise,以支持链式调用。链式调用是 Promise 的重要特性,允许我们将多个异步操作串联在一起,每个操作都依赖于前一个操作的结果。

当我们调用 then 方法时,它返回一个新的 Promise,这个新的 Promise 的状态取决于回调函数的执行结果。如果回调函数返回一个值,新的 Promise 将以这个值为结果。如果回调函数抛出错误,新的 Promise 将以这个错误为拒绝理由。

通过这种机制,我们可以连续调用多次 then 方法,每次都可以处理上一步的结果并返回一个新的 Promise,实现复杂的异步操作链。

new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
}).then(value => {
    console.log(value); // 1
    return value * 2;
}).then(value => {
    console.log(value); // 2
    return value * 3;
}).then(value => {
    console.log(value); // 6
}).catch(error => {
    console.error(error);
});

通过返回一个新的 Promise,我们的代码能够以链式结构编写,每个 then 的返回值都可以传递给后续的 then,使代码更加清晰和易于维护。

Step 4: 解析 resolvePromise 函数

resolvePromise 函数在 Promise 实现中扮演着关键角色。它用于处理 then 方法返回的结果,并确保其符合 Promise/A+ 规范。

const resolvePromise = (promise2, x, resolve, reject) => {
    if (x === promise2) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }

    let called = false; // 防止多次调用
    if (x != null && (typeof x === 'object' || typeof x === 'function')) {
        try {
            let then = x.then;
            if (typeof then === 'function') { 
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, err => {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
};

下面详细解析每个判断的作用和原因。

确保避免循环引用

if (x === promise2) {
    return reject(new TypeError('Chaining cycle detected for promise'));
}

这个判断是为了避免循环引用。如果 promise2 和 x 是同一个对象,则会导致无限循环,从而引发栈溢出错误。例如,以下代码会导致无限递归:

let promise = new Promise((resolve, reject) => {
    resolve();
}).then(() => {
    return promise; // 无限循环
});

通过这个检查,我们可以防止这种情况,避免产生不可控的错误。

处理对象或函数

if (x != null && (typeof x === 'object' || typeof x === 'function')) {

这个判断用于检测 x 是否是一个对象或函数。如果 x 是 null 或基本类型(如字符串、数字等),则直接调用 resolve 进行处理。如果 x 是对象或函数,则可能是一个 "thenable" 对象,我们需要进一步处理。例如,以下代码中传入了一个 thenable 对象:

let promise = new Promise((resolve, reject) => {
    resolve({
        then: function (onFulfilled) {
            onFulfilled('Hello');
        }
    });
});

检查并处理 then 方法

try {
    let then = x.then;
    if (typeof then === 'function') {
        then.call(x, y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
        }, err => {
            if (called) return;
            called = true;
            reject(err);
        });
    } else {
        resolve(x);
    }
} catch (e) {
    if (called) return;
    called = true;
    reject(e);
}

在这段代码中,我们尝试读取 xthen 方法,以确定 x 是否是一个 "thenable" 对象。如果 then 是一个函数,我们将 x 视为一个 Promise,并调用 then 方法。这样可以递归处理返回的新 Promise。如果 then 不是函数,则将 x 视为普通值,直接调用 resolve

防止多次调用

let called = false;

...

if (called) return;
called = true;

变量 called 用于防止回调函数被多次调用。这在处理 "thenable" 对象时尤其重要,因为根据 Promise/A+ 规范,一旦 resolvereject 被调用,状态就应被锁定,而 then 方法中的回调函数可能会被多次调用。

举个例子:

let obj = {
    then(resolve, reject) {
        resolve('First call');
        resolve('Second call');
    }
};

new Promise((resolve, reject) => {
    resolve(obj);
}).then(value => {
    console.log(value); // 输出: 'First call'
});

在这个例子中,objthen 方法中多次调用 resolve。由于 called 变量的存在,仅第一次调用有效,后续的调用将被忽略。

完整示例

通过 resolvePromise 函数,我们确保 then 方法的返回值能够正确处理和传递,并保证新返回的 Promise 符合 Promise/A+ 规范,从而实现链式调用和正确的异步行为。以下是完整的代码实现,包括对回调函数的异步处理(使用 queueMicrotask)和 then 方法返回新的 Promise:

const resolvePromise = (promise2, x, resolve, reject) => {
    if (x === promise2) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }

    let called = false; // 防止多次调用
    if (x != null && (typeof x === 'object' || typeof x === 'function')) {
        try {
            let then = x.then;
            if (typeof then === 'function') { 
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, err => {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
};

class Promise {
    constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = value => {
            if (this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        };

        const reject = reason => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

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

        const promise2 = new Promise((resolve, reject) => {
            if (this.state === 'fulfilled') {
                queueMicrotask(() => {
                    try {
                        const x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            } else if (this.state === 'rejected') {
                queueMicrotask(() => {
                    try {
                        const x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            } else if (this.state === 'pending') {
                this.onResolvedCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    });
                });
                this.onRejectedCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    });
                });
            }
        });

        return promise2;
    }

    catch(fn) {
        return this.then(null, fn);
    }
}

如何验证

在实际应用中,往往需要验证我们实现的 Promise 是否符合规范和预期。可以使用 promises-aplus-tests 插件来进行验证。

  1. 安装 promises-aplus-tests 插件:

    npm install -g promises-aplus-tests
    
  2. 在我们的代码结尾添加以下代码:

    Promise.defer = Promise.deferred = function () {
        let dfd = {};
        dfd.promise = new Promise((resolve, reject) => {
            dfd.resolve = resolve;
            dfd.reject = reject;
        });
        return dfd;
    }
    module.exports = Promise;
    
  3. 在我们的命令行运行测试:

    promises-aplus-tests path/to/your/promise-file.js