从零手写 Promise:彻底掌握异步编程的精髓
Promise 是现代 JavaScript 中处理异步操作的基石,它的出现解决了传统回调函数带来的“回调地狱”问题,让异步代码变得更加清晰和易于维护。理解 Promise 的工作原理,不仅能让你更好地使用它,更能让你对 JavaScript 的异步编程有更深刻的理解。
本文将通过手写一个完整的 Promise,并详细解析其源码,带你彻底掌握 Promise 的核心概念、工作流程以及常用扩展方法。
一、Promise 核心原理:状态机
Promise 本质上是一个状态机,它有三种状态:
- pending(进行中) :初始状态,既不是成功也不是失败。
- fulfilled(已成功) :操作成功完成的状态。
- rejected(已失败) :操作失败的状态。
这三种状态之间的转换是单向的:
pending可以转换为fulfilled。pending可以转换为rejected。
一旦状态从 pending 变为 fulfilled 或 rejected,就不能再改变了。这个“一次性”的特性是 Promise 稳定的关键。
二、手写 Promise 源码
我们来一步步实现一个完整的 MyPromise 类。
1. 骨架搭建
首先,我们需要一个构造函数来接收一个执行器函数 executor。executor 函数在 new MyPromise() 时会立即执行,并接收 resolve 和 reject 两个参数。
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);
}
}
}
代码解读:
- 我们用三个常量
PENDING、FULFILLED、REJECTED来表示状态,更易于维护。 - 构造函数中初始化了状态
status、成功值value和失败原因reason。 onFulfilledCallbacks和onRejectedCallbacks是两个数组,用来存储异步操作完成时需要执行的回调函数。这是实现then方法的关键。resolve和reject函数负责改变状态,并存储结果或原因。注意,它们只在pending状态下生效,保证了状态的不可逆。try...catch块是为了捕获executor函数中可能抛出的同步错误,确保任何错误都能被 Promise 捕获到并转换为rejected状态。
2. 实现 then 方法
then 方法是 Promise 链式调用的核心。它接收两个可选参数:onFulfilled 和 onRejected。
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 状态改变,
resolve或reject就会被触发,新创建的 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 的核心:
- 状态机:
pending、fulfilled、rejected三种状态的单向转换。 - 异步队列:通过回调队列机制,实现了在状态改变后执行相应的回调。
- 链式调用:
then方法返回一个新的 Promise,并根据then的返回值来决定这个新 Promise 的状态。 - 兼容性:通过
resolvePromise方法兼容了 Promise 本身以及其他thenable对象。
理解了这些,你不仅能更好地使用 Promise,还能轻松掌握 async/await(它其实是 Promise 的语法糖),从而在异步编程的世界里游刃有余。