引言
在 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);
});
}
})
}
}
感谢阅读,敬请斧正!