引言
在 JavaScript 的世界里,异步编程是处理复杂应用逻辑时不可或缺的部分,而 Promise 无疑是这一领域的核心角色。它巧妙地解决了回调地狱这一棘手难题,为异步操作提供了一种既优雅又强大的解决方案。然而,你是否曾好奇,我们日常频繁使用的 Promise 究竟是如何在幕后运作的呢?它背后的实现机制又是怎样的呢?本文将带你踏上一段探索之旅,从零开始手写一个 Promise 的实现,一步步揭开它神秘的面纱。通过这个过程,你不仅能深入理解 Promise 的工作原理,还能领悟其设计哲学,从而在实际开发中运用起来更加得心应手。
Promises/A+ 规范概述
Promises/A+ 规范为
Promise的行为提供了统一的标准,了解它是深入理解Promise实现的基础。
Promises/A+ 具有以下几个关键特性
-
初始化异步操作 :当创建一个 Promise 时,需要传入一个
executor函数。这个函数接收resolve和reject两个参数,它们分别用于在异步操作成功或失败时改变 Promise 的状态。例如:tsconst likePromise = new LikePromise((resolve, reject) => { // 这里可以开始异步操作,如发起网络请求或读取文件等; // 当操作成功时,调用 resolve(result); // 当操作失败时,调用 reject(error); }); -
Promise的三种状态 :一个 Promise 在其生命周期内只能处于三种状态之一:等待态(Pending)、完成态(Fulfilled)或拒绝态(Rejected)。初始时
Promise处于等待态,当异步操作成功完成后,通过resolve函数将其状态转变为完成态,并传递成功结果;若异步操作失败,则通过reject函数将状态转变为拒绝态,并传递失败原因。 -
then方法的规范行为 :Promise 必须提供一个then方法,它允许为 Promise 的成功和失败状态分别指定处理函数。then方法的链式调用以及错误处理都有着严格的规范要求,这使得我们能够以一种条理清晰、连贯有序的方式去处理异步操作最终的结果和可能出现的错误。
深入剖析 Promise 创建时的 executor 函数
executor 函数的执行时机
executor函数是同步执行的,这一点需要特别注意
ts
const p = new LikePromise((resolve, reject) => {
console.log("likePromise-executor");
});
console.log("end");
// 输出:
// "likePromise-executor"
// "end"
从输出结果可以清晰地看到,executor 函数内的代码会先于后续的 console.log("end") 执行,这体现了它同步执行的特性。
executor 函数中的错误处理
1.同步错误处理 当executor 函数在同步执行过程中出现错误,Promise 会捕获该错误,并将自身状态修改为拒绝态(Rejected)。
ts
const p = new LikePromise((resolve, reject)=>{
throw new Error("出错了");
}).then(undefined, (res)=>{
console.log(res)
})
console.log("end");
// 输出:
// "end"
// "出错了"
在上述代码中,executor 函数内直接抛出了一个错误,此时 Promise 会捕获这个错误,将状态转变为拒绝态,后续通过 then 方法的第二个参数(处理拒绝态的回调函数)就可以获取到这个错误信息并进行相应处理。
2.异步错误处理 当executor 函数中的异步操作 出现错误(如在 setTimeout 回调中抛出错误),Promise 本身并不会直接捕获该异步错误。这是因为在异步操作发生错误时,无法确定何时应该处理该错误,所以需要在异步操作的回调函数中自行处理并调用 reject 函数来改变 Promise 的状态。
ts
const p = new LikePromise((resolve, reject)=>{
setTimeout(()=>{
try {
throw new Error("出错了");
} catch(error){
reject(error);
}
})
}).then(undefined, (res)=>{
console.log(res)
})
console.log("end");
在这个例子里,我们在 setTimeout 的回调函数中手动捕获错误,并通过 reject 函数将错误传递出去,以便后续通过 then 方法来处理这个拒绝态的情况。
Promise 状态流转&最终值的设置
状态流转规则
Promise 的状态流转有着严格的规则,只能在 resolve 和 reject 函数中进行操作,并且状态一旦发生改变,就是不可逆的。也就是说,一旦从等待态(Pending)转变为完成态(Fulfilled)或者拒绝态(Rejected),就再也不能切换回其他状态了。
ts
const p = new LikePromise((resolve, reject) => {
resolve(1);
// 后续再次调用 resolve 或 reject 都不会改变已经确定的状态
reject(2) // 这个是无效的,无法再次改变状态
});
在这里,当调用了 resolve(1) 后,Promise 的状态已经变为完成态,后续再调用 reject(2) 是不会起作用的,状态不会再发生改变。
最终值的设置
当 Promise 状态转变为完成态时,通过 resolve 函数传递的值将会成为这个 Promise 的最终值;而当状态转变为拒绝态时,通过 reject 函数传递的原因就会作为最终值。
ts
const p = new LikePromise((resolve, reject) => {
resolve("成功结果");
});
p.then(result => {
console.log(result); // 输出:"成功结果"
});
// ---
const p1 = new LikePromise((resolve, reject) => {
reject("拒绝原因");
});
p1.then(undefined, reason => {
console.log(reason); // 输出:"拒绝原因"
});
Promise 的 then 方法的实现
then 方法的链式调用原理
then 方法之所以能够实现链式调用,关键在于它返回一个新的 Promise 实例。如果直接返回 this,由于 Promise 的状态一旦改变就不可逆转,后续链式调用的 then 方法将无法正确处理不同的状态。
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
then(onFulfilled, onRejected) {
// 错误的返回方式,不能直接返回 this
// return this;
// 正确的返回方式
return new LikePromise((resolve, reject)=>{
// TODO
})
}
}
then 方法中回调函数的执行时机
then 方法的回调函数应该在 Promise 状态确定后异步执行 。为了确保这一点,我们可以借助微任务队列(如 queueMicrotask、process.nextTick 或通过 MutationObserver 模拟)来确保回调函数在当前宏任务执行完后、下一个宏任务开始前执行。
ts
function isPromiseA(value){
return value && typeof value.then === 'function';
}
function microTask(fn) {
// Promise.resolve().then(fn); 原生Promise.then包装是异步任务
if (typeof queueMicrotask === 'function') {
queueMicrotask(fn);
} else if(typeof process === 'object' && typeof process.nextTick === 'function') {
// Node 环境下
process.nextTick(fn);
} else if(typeof MutationObserver === 'function') {
const textNode = document.createTextNode('');
const observer = new MutationObserver(fn);
observer.observe(textNode, { characterData: true });
textNode.data = 'promise';
} else {
setTimeout(fn, 0);
}
}
class LikePromise {
constructor(executor) {
// 初始化代码
}
then(onFulfilled, onRejected) {
return new LikePromise((resolve, reject)=>{
this.handlers.push(microTask(()=>{
try {
const cb = this.state === STATE.FULFILLED ? onFulfilled : onRejected;
if (cb && typeof cb === "function") {
const data = cb(this.result);
if (isPromiseA(data)) {
data.then(resolve, reject);
} else {
resolve(data);
}
}else{
resolve(this.result);
}
} catch (error) {
reject(error);
}
}))
})
}
}
在上述代码中,通过 microTask 函数将回调函数放入微任务队列,保证了其异步执行的特性,并且在回调函数内部对返回值进行了相应处理,以符合 Promise 的规范要求。
then 方法对回调函数的具体处理
1.默认值处理 若没传 onFulfilled、onRejected 回调,就会默认获取之前 Promise 的最终值并传给下一个 then方法。
ts
const p = new LikePromise((resolve, reject) => {
resolve("成功结果");
}).then().then();
p.then(result => {
console.log(result); // 输出:"成功结果"
});
从这个例子可以看到,即使中间的 then 方法没有传递具体的回调,依然可以正确获取到最初 Promise 的成功结果,这就是默认值处理机制在起作用。
2.函数返回值处理 当传入的 onFulfilled 或 onRejected 是函数时,会在 Promise 状态确定后被调用。调用后要判断返回值:如果是符合 Promises/A+ 规范的对象(有 then 方法),就调用它的 then 方法并传入新 Promise 的 resolve 和 reject,以便处理其状态流转;若不是,就直接用新 Promise 的 resolve 传递返回值,让后续 then 能接着处理。
ts
const p = new LikePromise((resolve, reject) => {
resolve("成功结果");
}).then((res)=>{
return new LikePromise((resolve, reject)=>{
resolve("then-返回Promise")
})
}).then();
p.then(result => {
console.log(result); // 输出:"then-返回Promise"
});
在这段代码中,第一个 then 方法返回了一个新的 Promise,后续的 then 方法能够正确获取到这个新 Promise 中传递的值,体现了对函数返回值的正确处理方式。
LikePromise 源码
ECMAScript 6 Promise 静态方法与实例方法
catch 实现
catch方法实际上是Promise.prototype.then(undefined, onRejected)的一种简写形式
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
catch(onRejected){
return this.then(undefined, onRejected)
}
}
通过这样简单的实现,我们就可以方便地使用 catch 方法来统一处理 Promise 被拒绝的情况了。
finally 实现
finally方法对应的是 Promise.prototype.then(onFinally, onFinally)
onFinally不接受参数finally方法是不修改Promise状态的
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
finally(onFinally){
return this.then((value)=>{
onFinally();
return value;
}, (reason)=>{
onFinally();
// 如果这里直接return 那么会修改状态为完成
// return reason;
// 通过抛出异常来保持状态不变
throw reason;
})
}
}
通过上述代码实现,finally 方法就能在无论 Promise 成功还是失败的情况下,都执行指定的最终操作,同时不改变原有的状态。
resolve 实现
resolve 方法的描述
resolve方法的实现遵循以下逻辑:
- 首先判断传入的参数是否是
Promise,如果是,直接返回该Promise。 - 接着判断参数是否是
isPromiseA(也就是thenable对象,即具有then方法的对象),若是,则调用其then方法并传入resolve和reject两个回调函数。 - 最后,如果都不符合上述情况,就使用
resolve函数进行包裹返回。
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
static resolve(value?: any) {
if(value instanceof Promise) return value;
return new Promise((resolve, reject) => {
if (isPromiseA(value)) {
value.then(resolve, reject);
}else{
resolve(value);
}
})
}
}
reject 实现
reject 方法的实现相对比较简单,它的作用就是返回一个新的 Promise,并将传入的参数作为拒绝的原因。代码如下:
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
static reject(reason?: any) {
return new Promise((_, reject) => {
reject(reason);
})
}
}
all 实现
all 方法用于处理多个 Promise 实例,它会等待所有传入的 Promise 都完成后,将它们各自的结果组成一个数组返回,如果其中有任何一个 Promise 被拒绝,那么整个 all 方法返回的 Promise 就会立即被拒绝,并传递出第一个被拒绝的 Promise 的原因。
ts
class LikePromise {
constructor(executor) {
// 初始化代码
}
static all(promises: Promise<any>[]) {
return new Promise((resolve, reject) => {
let count = 0;
const results: any[] = [];
for (let i = 0; i < promises.length; i++) {
promises[i].then((value) => {
results[i] = value;
count++;
if (count === promises.length) {
resolve(results);
}
}, (reason)=>{
reject(reason);
});
}
})
}
}
感谢阅读,敬请斧正!