从零手写 Promise:彻底掌握异步编程的精髓

191 阅读8分钟

从零手写 Promise:彻底掌握异步编程的精髓

Promise 是现代 JavaScript 中处理异步操作的基石,它的出现解决了传统回调函数带来的“回调地狱”问题,让异步代码变得更加清晰和易于维护。理解 Promise 的工作原理,不仅能让你更好地使用它,更能让你对 JavaScript 的异步编程有更深刻的理解。

本文将通过手写一个完整的 Promise,并详细解析其源码,带你彻底掌握 Promise 的核心概念、工作流程以及常用扩展方法。


一、Promise 核心原理:状态机

Promise 本质上是一个状态机,它有三种状态:

  • pending(进行中) :初始状态,既不是成功也不是失败。
  • fulfilled(已成功) :操作成功完成的状态。
  • rejected(已失败) :操作失败的状态。

这三种状态之间的转换是单向的:

  • pending 可以转换为 fulfilled
  • pending 可以转换为 rejected

一旦状态从 pending 变为 fulfilledrejected,就不能再改变了。这个“一次性”的特性是 Promise 稳定的关键。


二、手写 Promise 源码

我们来一步步实现一个完整的 MyPromise 类。

1. 骨架搭建

首先,我们需要一个构造函数来接收一个执行器函数 executorexecutor 函数在 new MyPromise() 时会立即执行,并接收 resolvereject 两个参数。

JavaScript

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

class MyPromise {
    constructor(executor) {
        // 初始化状态
        this.status = PENDING;
        // 存储成功时的值
        this.value = undefined;
        // 存储失败时的原因
        this.reason = undefined;

        // 成功回调函数队列
        this.onFulfilledCallbacks = [];
        // 失败回调函数队列
        this.onRejectedCallbacks = [];

        // 成功函数
        const resolve = (value) => {
            // 只有在 pending 状态时才能改变状态
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 状态改变后,依次执行成功回调
                this.onFulfilledCallbacks.forEach(cb => cb());
            }
        };

        // 失败函数
        const reject = (reason) => {
            // 只有在 pending 状态时才能改变状态
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 状态改变后,依次执行失败回调
                this.onRejectedCallbacks.forEach(cb => cb());
            }
        };

        try {
            // 立即执行执行器函数
            executor(resolve, reject);
        } catch (e) {
            // 捕获执行器中的错误,直接 reject
            reject(e);
        }
    }
}

代码解读:

  • 我们用三个常量 PENDINGFULFILLEDREJECTED 来表示状态,更易于维护。
  • 构造函数中初始化了状态 status、成功值 value 和失败原因 reason
  • onFulfilledCallbacksonRejectedCallbacks 是两个数组,用来存储异步操作完成时需要执行的回调函数。这是实现 then 方法的关键。
  • resolvereject 函数负责改变状态,并存储结果或原因。注意,它们只在 pending 状态下生效,保证了状态的不可逆。
  • try...catch 块是为了捕获 executor 函数中可能抛出的同步错误,确保任何错误都能被 Promise 捕获到并转换为 rejected 状态。

2. 实现 then 方法

then 方法是 Promise 链式调用的核心。它接收两个可选参数:onFulfilledonRejected

JavaScript

// 接 MyPromise 类
class MyPromise {
    // ... (构造函数不变)

    then(onFulfilled, onRejected) {
        // 为确保链式调用,then 方法必须返回一个新的 Promise
        return new MyPromise((resolve, reject) => {
            // 封装成功时的处理逻辑
            const handleFulfilled = () => {
                // 使用 setTimeout 模拟异步,确保 then 的回调总是异步执行
                setTimeout(() => {
                    try {
                        // 如果 onFulfilled 不是函数,直接将值传递给下一个 then
                        if (typeof onFulfilled !== 'function') {
                            resolve(this.value);
                            return;
                        }
                        const x = onFulfilled(this.value);
                        // 处理返回值,这是 then 方法的核心
                        this.resolvePromise(x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            };

            // 封装失败时的处理逻辑
            const handleRejected = () => {
                setTimeout(() => {
                    try {
                        // 如果 onRejected 不是函数,直接将失败原因传递给下一个 then
                        if (typeof onRejected !== 'function') {
                            reject(this.reason);
                            return;
                        }
                        const x = onRejected(this.reason);
                        this.resolvePromise(x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            };

            // 根据状态执行不同的逻辑
            if (this.status === FULFILLED) {
                handleFulfilled();
            } else if (this.status === REJECTED) {
                handleRejected();
            } else if (this.status === PENDING) {
                // 如果是 pending 状态,将回调存入队列
                this.onFulfilledCallbacks.push(handleFulfilled);
                this.onRejectedCallbacks.push(handleRejected);
            }
        });
    }

    // 处理 then 返回值的核心逻辑
    resolvePromise(x, resolve, reject) {
        // 如果返回的 x 和 Promise 是同一个,会造成循环引用
        if (x === this) {
            return reject(new TypeError('Chaining cycle detected for promise'));
        }

        let called = false; // 避免重复调用

        // 如果 x 是一个 Promise
        if (x instanceof MyPromise) {
            x.then(y => {
                this.resolvePromise(y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        }
        // 如果 x 是一个对象或函数(thenable)
        else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
            try {
                const then = x.then;
                if (typeof then === 'function') {
                    // 调用 then 方法,并确保 this 指向 x
                    then.call(x, y => {
                        if (called) return;
                        called = true;
                        this.resolvePromise(y, resolve, reject);
                    }, r => {
                        if (called) return;
                        called = true;
                        reject(r);
                    });
                } else {
                    resolve(x);
                }
            } catch (e) {
                if (called) return;
                called = true;
                reject(e);
            }
        }
        // 如果 x 是普通值
        else {
            resolve(x);
        }
    }
}

代码解读:

  • then 方法返回一个新的 Promise:这是实现链式调用的关键。

  • 微任务队列:我们使用 setTimeout(..., 0) 来模拟 Promise 规范中的微任务(microtask)机制。在浏览器中,Promise 的回调是在微任务队列中执行的,这比宏任务(如 setTimeout)优先级更高。

  • 回调非函数处理:如果 then 的参数不是函数,例如 promise.then().then(...),我们需要将值或原因直接透传下去。

  • resolvePromise 核心:这是 Promise 规范中最复杂的部分,用于处理 then 的返回值。

    • 循环引用:如果 then 返回了自身,会陷入无限循环,需要抛出 TypeError
    • 返回 Promise:如果 then 返回了一个新的 Promise,我们需要等这个 Promise 状态确定后,再决定我们返回的新 Promise 的状态。这是一种递归处理。
    • 返回 thenable 对象:如果返回值是一个具有 then 方法的对象,我们也需要调用它的 then 方法来决定最终状态。这是 Promise 兼容其他异步库的关键。
    • 返回普通值:直接用这个值 resolve 掉返回的新 Promise。

三、扩展方法实现

1. MyPromise.resolve()MyPromise.reject()

这是创建 Promise 的快捷方式。

JavaScript

MyPromise.resolve = function(value) {
    if (value instanceof MyPromise) {
        return value;
    }
    return new MyPromise(resolve => resolve(value));
};

MyPromise.reject = function(reason) {
    return new MyPromise((resolve, reject) => reject(reason));
};
  • resolve(value):如果 value 本身是一个 Promise,则直接返回它;否则,返回一个状态为 fulfilled 的 Promise。
  • reject(reason):返回一个状态为 rejected 的 Promise。

2. MyPromise.all()

Promise.all() 接收一个 Promise 数组,等所有 Promise 都成功后,返回一个包含所有结果的数组;只要有一个失败,就立即返回失败的结果。

JavaScript

MyPromise.all = function(promises) {
    return new MyPromise((resolve, reject) => {
        let results = [];
        let completed = 0;
        let total = promises.length;

        if (total === 0) {
            return resolve([]);
        }

        promises.forEach((promise, index) => {
            // 将非 Promise 值包装成 Promise
            MyPromise.resolve(promise).then(value => {
                results[index] = value;
                completed++;
                if (completed === total) {
                    resolve(results);
                }
            }, reason => {
                reject(reason); // 只要有一个失败,立即返回
            });
        });
    });
};

代码解读:

  • 使用 results 数组按顺序存储每个 Promise 的结果。
  • 使用 completed 计数器记录已完成的 Promise 数量。
  • completed 等于 total 时,说明所有 Promise 都成功,可以 resolve
  • 只要有一个 Promise 失败,就立即 reject

3. MyPromise.race()

Promise.race() 接收一个 Promise 数组,只要其中一个 Promise 率先完成(成功或失败),就立即返回该 Promise 的结果。

JavaScript

MyPromise.race = function(promises) {
    return new MyPromise((resolve, reject) => {
        promises.forEach(promise => {
            // 将非 Promise 值包装成 Promise
            MyPromise.resolve(promise).then(value => {
                resolve(value); // 只要有一个成功,就立即 resolve
            }, reason => {
                reject(reason); // 只要有一个失败,就立即 reject
            });
        });
    });
};

代码解读:

  • 它比 all 简单很多,因为不需要等待所有 Promise 完成。
  • 一旦数组中的任何一个 Promise 状态改变,resolvereject 就会被触发,新创建的 Promise 也会随之改变状态。

四、Promise 应用实例

掌握了 Promise 的原理,我们来看几个实用的应用场景。

1. 链式异步操作

经典的异步操作序列,例如先登录,然后获取用户信息,再获取用户的朋友列表。

JavaScript

function login() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('登录成功');
            resolve('user_token');
        }, 1000);
    });
}

function getUserInfo(token) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('获取用户信息成功', token);
            resolve({ name: 'Alice', id: 123 });
        }, 800);
    });
}

function getFriends(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('获取朋友列表成功', user.name);
            resolve(['Bob', 'Charlie']);
        }, 500);
    });
}

// 链式调用
login()
    .then(token => getUserInfo(token))
    .then(user => getFriends(user))
    .then(friends => {
        console.log('最终结果:', friends);
    })
    .catch(err => {
        console.error('发生错误:', err);
    });

这种方式避免了层层嵌套的回调函数,代码结构扁平且易读。

2. 并行请求

同时向多个 API 发送请求,并等待所有请求都返回。

JavaScript

const fetchProducts = () => new MyPromise(res => setTimeout(() => res({ products: ['P1', 'P2'] }), 1500));
const fetchCategories = () => new MyPromise(res => setTimeout(() => res({ categories: ['C1', 'C2'] }), 1000));
const fetchUser = () => new MyPromise(res => setTimeout(() => res({ user: 'Alice' }), 800));

MyPromise.all([fetchProducts(), fetchCategories(), fetchUser()])
    .then(results => {
        // results 是一个包含所有成功结果的数组
        const [productsData, categoriesData, userData] = results;
        console.log('所有数据已获取:', {
            products: productsData.products,
            categories: categoriesData.categories,
            user: userData.user
        });
    })
    .catch(err => {
        console.error('有请求失败了:', err);
    });

3. 超时控制

使用 Promise.race 来为请求设置一个超时时间。

JavaScript

function fetchWithTimeout(url, timeout) {
    // 创建一个会超时的 Promise
    const timeoutPromise = new MyPromise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error(`请求超时,超过 ${timeout}ms`));
        }, timeout);
    });

    // 真正的请求
    const requestPromise = new MyPromise(resolve => {
        setTimeout(() => {
            console.log(`请求 ${url} 完成`);
            resolve(`数据来自 ${url}`);
        }, 2000); // 假设这个请求需要 2000ms
    });

    return MyPromise.race([requestPromise, timeoutPromise]);
}

// 设置 1500ms 超时
fetchWithTimeout('https://api.example.com', 1500)
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error(err.message); // 输出:请求超时,超过 1500ms
    });

总结

通过手写 MyPromise,我们深入理解了 Promise 的核心:

  • 状态机pendingfulfilledrejected 三种状态的单向转换。
  • 异步队列:通过回调队列机制,实现了在状态改变后执行相应的回调。
  • 链式调用then 方法返回一个新的 Promise,并根据 then 的返回值来决定这个新 Promise 的状态。
  • 兼容性:通过 resolvePromise 方法兼容了 Promise 本身以及其他 thenable 对象。

理解了这些,你不仅能更好地使用 Promise,还能轻松掌握 async/await(它其实是 Promise 的语法糖),从而在异步编程的世界里游刃有余。