《Promise 深度解析与手写实现:异步编程的进阶之路》

148 阅读7分钟

一、回调地狱与Promise的引入

在编程中,回调地狱是一个令人头疼的问题。举个生活中的例子:小明去自助烤肉店吃饭,由于自助餐不允许浪费,所以小明每次都把拿到的食物吃完,然后判断是否吃饱。如果没吃饱,就继续拿餐;吃饱了就停止。

用代码来实现这个场景,可能会是这样的:

const sendMessage = (food, onFulfilled, onRejected) => {
    console.log(`烤肉店,去拿${food}...`);
    console.log(`干饭中...`);
    setTimeout(() => {
        // 模拟是否吃饱
        if (Math.random() <= 0.1) {
            console.log(`吃了${food}`);
            onFulfilled();
        } else {
            console.log(`吃了${food}`);
            onRejected();
        }
    }, 1000);
};
sendMessage(
    "五花肉",
    () => {
        console.log("吃饱了...");
    },
    () => {
        console.log("没吃饱..");
        sendMessage(
            "牛肉",
            () => {
                console.log("吃饱了...");
            },
            () => {
                console.log("没吃饱..");
                sendMessage(
                    "鸡翅",
                    () => {
                        console.log("吃饱了...");
                    },
                    () => {
                        console.log("没吃饱...");
                        console.log("没食物了...");
                    }
                );
            }
        );
    }
);

可以看到,代码中嵌套了大量的回调函数,这种结构不仅难以阅读,而且难以维护。每次“没吃饱”都需要在 onRejected 中进行处理,如果一直没吃饱,就会不断嵌套回调,导致代码变得非常复杂。

小结:回调地狱是指在回调函数中嵌套更多的回调函数,这种结构会导致代码难以理解和维护。为了解决回调地狱的问题,我们需要引入 Promise

二、Promise规范

Promise 是一套专门用于处理异步场景的规范,它能够有效避免回调地狱的产生,使异步代码更加清晰、简洁和统一。这套规范最早诞生于前端社区,规范名称为 Promise A+

Promise A+ 规范的核心内容如下:

1、任务对象:所有的异步场景都可以看作一个异步任务,每个异步任务在 JavaScript 中表现为一个对象,即 Promise 对象,也称为任务对象。

classDiagram
class Promise对象1{
 网络通信请求
}
class Promise对象2{
 网络通信请求
}
class Promise对象{
 .....
}

2、任务状态:每个任务对象都有两个阶段和三个状态。两个阶段是未决阶段(pending)和已决阶段(settled);三个状态是挂起状态(pending)、完成状态(fulfilled)和失败状态(rejected)。任务总是从未决阶段变为已决阶段,状态不能倒流。一旦任务完成或失败,状态就固定下来,永远无法改变。

classDiagram

class 未决阶段unsettled{
 pending 挂起状态
}

class 已决阶段settled{
 fulfilled 完成状态
 rejected 失败状态
}
未决阶段unsettled --|> 已决阶段settled

3、状态转换:从挂起状态到完成状态称为 resolve,从挂起状态到失败状态称为 reject。任务完成时可能有一个相关数据,任务失败时可能有一个失败原因

classDiagram

class 未决阶段unsettled{
 pending 挂起状态
}

class 已决阶段settled{
 fulfilled 完成状态
 rejected 失败状态
}
未决阶段unsettled --|> 已决阶段settled:resolved(data)改变完成状态
未决阶段unsettled --|> 已决阶段settled:reject(reason)改变失败状态

4、后续处理:可以针对任务进行后续处理,针对完成状态的后续处理称为 onFulfilled,针对失败状态的后续处理称为 onRejected

classDiagram

class 未决阶段unsettled{
 pending 挂起状态
}

class 已决阶段settled{
 fulfilled 完成状态
 rejected 失败状态
}
class 后续任务完成后处理data {
 onFulfilled()
 onRejected()
}
未决阶段unsettled --|> 已决阶段settled:resolved(data)改变完成状态
未决阶段unsettled --|> 已决阶段settled:reject(reason)改变失败状态
已决阶段settled --|> 后续任务完成后处理data:onFulfilled
已决阶段settled --|> 后续任务完成后处理data:rejected

通过这些规范,Promise 能够以一种更加优雅的方式处理异步任务。

三、Promise API

ES6 提供了一套 API,实现了 Promise A+ 规范。基本使用如下:

const promise = new Promise((resolve, reject) => {
    // 任务具体执行流程,该函数会立即执行
    // 调用 resolve(data),可将任务变为 fulfilled 状态,data 为需要传递的相关数据
    // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的原因
});

promise.then(
    (data) => {
        // onFulfilled 函数,当任务完成后自动运行该函数,data 为任务完成时的相关数据
    },
    (reason) => {
        // onRejected 函数,当任务失败后,会自动运行该函数,reason 为任务失败的相关原因
    }
);

通过Promise改造上面代码

const sendMessage = (food) => {
    return new Promise((resolve, reject) => {
        console.log(`烤肉店,去拿${food}...`);
        console.log(`干饭中...`);
        setTimeout(() => {
            // 模拟是否吃饱
            if (Math.random() <= 0.1) {
                resolve(`吃了${food},吃饱了`);
            } else {
                reject(`吃了${food},没吃饱`);
            }
        }, 1000);
    });
};
sendMessage("五花肉").then(
    (res) => {
        console.log(res);
    },
    (error) => {
        console.log(error);
    }
);

虽然代码有所改进,但仍然没有完全解决回调地狱的问题。接下来,我们继续学习 Promise 的其他特性。

补充一个 API:catch

.catch(onRejected) 等同于 .then(null, onRejected)

四、链式调用

Promise 的一个重要特性是链式调用。then 方法必定会返回一个新的 Promise,可以理解为后续处理也是一个 Promise 任务。新任务的状态取决于后续处理的结果。

graph TD
Promise1 -->|API| then1
onFulfilled1 -->|处理成功数据data|then1
onRejected1 -->|处理失败原因reason|then1
then1 -->|then方法调用返回一个新的Promise| Promise2
Promise2 -->|API| then2
onFulfilled2 -->|处理成功数据data|then2
onRejected2 -->|处理失败原因reason|then2
then2 -->|then方法调用返回一个新的Promise| Promise3

1、如果没有相关后续处理,新任务状态和前任务一致,数据为前任务的数据。

2、如果有后续处理但还未执行,新任务挂起。

3、如果后续处理执行了,则根据后续处理的情况确定新任务的状态:

  • 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值。
  • 后续处理执行有错,新任务的状态为失败,数据为异常对象。
  • 后续执行后返回的是一个任务对象,新任务的状态和数据对象与该任务一致。

通过链式调用,异步代码的表达能力得到了极大的提升。例如:

// 任务成功,执行处理 1,失败执行处理 2
promise.then(处理1).catch(处理2);

// 任务成功,依次执行处理 1 和处理 2,若任务失败或者前面的处理有错,执行处理 3
promise.then(处理1).then(处理2).catch(处理3);

有了链试调用,上述案例代码继续优化

const sendMessage = (food) => {
   return new Promise((resolve, reject) => {
       console.log(`烤肉店,去拿${food}...`);
       console.log(`干饭中...`);
       setTimeout(() => {
           // 模拟是否吃饱
           if (Math.random() <= 0.1) {
               resolve(`吃了${food},吃饱了`);
           } else {
               reject(`吃了${food},没吃饱`);
           }
       }, 1000);
   });
};
sendMessage("五花肉")
   .catch((error) => {
       console.log(error);
       return sendMessage("牛肉");
   })
   .catch((error) => {
       console.log("鸡翅");
       return sendMessage("鸡翅");
   })
   .then(
       (res) => {
           console.log(res);
       },
       (error) => {
           console.log(error, "没吃的了");
       }
   );

通过链式调用,我们终于解决了回调地狱的问题。

五、Promise的静态方法

在实际开发中,我们常常会遇到多个异步任务需要同时处理的情况。例如,小明的媳妇给小明交代了两个任务:洗衣服(交给洗衣机完成)和拖地(交给扫地机器人完成)。小明需要在所有任务完成之后汇总结果,统一处理。

每个任务可以看作是一个返回 Promise 的函数:

const wash = () => {
   return new Promise((resolve, rejected) => {
       console.log("小明打开了洗衣机");
       setTimeout(() => {
           if (Math.random() < 0.5) {
               resolve("衣服洗好了");
           } else {
               rejected("忘记加水了,把衣服整烂了");
           }
       }, 1000);
   });
};
const sweep = () => {
   return new Promise((resolve, rejected) => {
       console.log("小明打开了扫地机器人");
       setTimeout(() => {
           if (Math.random() < 0.5) {
               resolve("地扫好了");
           } else {
               rejected("机器人坏了,没有扫地");
           }
       }, 2000);
   });
};

串行执行

如果按照串行的方式执行,先洗衣服,衣服洗完之后再拖地:

wash().then(() => {
   sweep();
});

这种方式虽然简单,但效率较低,因为两个任务无法同时进行。

并行执行

如果按照并行的方式执行,同时进行洗衣服和拖地:

wash();
sweep();

这种方式虽然提高了效率,但无法汇总任务状态,无法知道哪些任务完成了,哪些任务失败了。

为了解决这个问题,Promise 提供了以下静态方法:

1、Promise.resolve(data) :直接返回一个完成状态的任务。

2、 Promise.reject(reason) :直接返回一个拒绝状态的任务。

上面两种语法没啥特殊的等于以下写法

const promise = new Promise((resolve,reject)=>{
    resolve("成功")
})
const promise = new Promise((resolve,reject)=>{
    reject("失败")
})

3、Promise.all(任务数组) :返回一个任务,任务数组全部成功则成功,任何一个失败则失败。

4、Promise.any(任务数组) :返回一个任务,任务数组任一成功则成功,任务全部失败则失败。

代码理解

 const promise = Promise.all([Promise.resolve("成功"), Promise.reject("失败")]);
 //这是一个失败状态
 promise.then(null, (error) => {
   console.log(error);
 });

const promise = Promise.any([Promise.resolve("成功"), Promise.reject("失败")]);
 //这是一个成功的状态
 promise.then(()=>{
     console.log("成功了")
 });

5、Promise.allSettled(任务数组) :返回一个任务,任务数组全部已决则成功,该任务不会失败。只有状态都从 pending 状态改变才会执行 then

  const pro1 = () =>
   new Promise((resolve, reject) => {
     setTimeout(() => {
       resolve("成功了1");
     }, 2000);
   });
 const pro2 = () =>
   new Promise((resolve, reject) => {
     setTimeout(() => {
       resolve("失败了1");
     }, 4000);
   });
 const promise = Promise.allSettled([pro1(), pro2()]);
 //这是一个失败状态
 promise.then(
   (data) => {
     console.log(data, "成功了");
   },
   (error) => {
     console.log(error);
   }
 );

6、 Promise.race(任务数组) :返回一个任务,任务数组任一已决则已决,状态和其一致。只要状态有一个从 pending 状态改变就会执行 then

  const pro1 = () =>
   new Promise((resolve, reject) => {
     setTimeout(() => {
       resolve("成功了1");
     }, 2000);
   });
 const pro2 = () =>
   new Promise((resolve, reject) => {
     setTimeout(() => {
       resolve("失败了1");
     }, 4000);
   });
 const promise = Promise.race([pro1(), pro2()]);
 //这是一个失败状态
 promise.then(
   (data) => {
     console.log(data, "成功了");
   },
   (error) => {
     console.log(error);
   }
 );

对于小明的任务,最适合使用 Promise.allSettled

Promise.allSettled([wash(), sweep()]).then((data) => {
   console.log(data);
});

通过这些静态方法,我们可以更加灵活地处理多个异步任务。

六、 回调消除:Async 和 Await

Promise 的出现极大地简化了异步代码的编写,但为了进一步提高代码的可读性和可维护性,ES2017 引入了 asyncawait 关键字。

async

const method1 = async () => {
    return 1; // 该函数的返回值是 Promise 完成后的数据
};
method1(); // Promise {1}

const method2 = async () => {
    return Promise.resolve(1); // 若返回的是 Promise,则 method2 的返回值与该 Promise 状态一致
};
method2(); // Promise {1}

const method3 = async () => {
    throw new Error("1"); // 若执行过程报错,则任务是 rejected
};
method3(); // Promise{<rejected> Error(1)}

await

await 关键字表示等待某个 Promise 完成,它必须用于 async 函数中:

async function method() {
    const n = await Promise.resolve(1);
    console.log(n);
}
// 上面的函数等同于
function method1() {
    return new Promise((resolve, reject) => {
        Promise.resolve(1).then((n) => {
            console.log(n);
            resolve(1);
        });
    });
}

await 必须在 async 函数中使用。

通过 asyncawait,我们可以以同步的方式编写异步代码,使代码更加简洁易读。

七、手写Promise

虽然在实际开发中,我们通常不需要手写 Promise,但在面试中,手写 Promise 是一个常见的考察点。此外,通过手写 Promise,我们可以更好地理解其内部实现原理,提升逻辑思维能力。

说明

本篇文章不会百分之百还原Promise A+规范,但是会还原它核心的百分之八九十

实现状态变化

Promise 是一个构造函数,我们可以使用 ES6 的 class 来实现:

const isPromise = (obj) => { 
    return !!obj && typeof obj === "object" && typeof obj.then === "function"; 
};
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
    constructor(executor) {
        this.state = PENDING;
        this.value = undefined;
        this.#handlers = [];
        try {
            executor(this.#resolve.bind(this), this.#reject.bind(this));
        } catch (error) {
            this.#reject(error);
        }
    }

    #changeState(newState, value) {
        if (this.state !== PENDING) return;
        this.state = newState;
        this.value = value;
        this.#runHandlers();
    }

    #resolve(data) {
        this.#changeState(FULFILLED, data);
    }

    #reject(reason) {
        this.#changeState(REJECTED, reason);
    }
}

.then()

then 函数接收两个参数 onFulfilledonRejected,并返回一个新的 Promisethen 函数中的回调函数会在微任务队列中执行:

const runMicroTask = (callback) => {
    if (process?.nextTick) {
        process.nextTick(callback);
    } else if (MutationObserver) {
        const p = document.createElement("p");
        const observer = new MutationObserver(callback);
        observer.observe(p, { childList: true });
        p.innerHTML = "promise mock";
    } else {
        setTimeout(callback);
    }
};

class MyPromise {
    then(onFulfilled, onRejected) {
        return new MyPromise((resolve, reject) => {
            this.#pushHandler(onFulfilled, FULFILLED, resolve, reject);
            this.#pushHandler(onRejected, REJECTED, resolve, reject);
            this.#runHandlers();
        });
    }

    #pushHandler(executor, state, resolve, reject) {
        this.#handlers.push({ executor, state, resolve, reject });
    }

    #runHandlers() {
        if (this.state === PENDING) return;
        while (this.#handlers[0]) {
            this.#runOneHandler(this.#handlers[0]);
            this.#handlers.shift();
        }
    }

    #runOneHandler({ executor, state, resolve, reject }) {
        runMicroTask(() => {
            if (this.state !== state) return;
            if (typeof executor !== "function") {
                this.state === FULFILLED ? resolve(this.value) : reject(this.value);
                return;
            }
            try {
                const result = executor(this.value);
                if (isPromise(result)) {
                    result.then(resolve, reject);
                } else {
                    resolve(result);
                }
            } catch (error) {
                reject(error);
            }
        });
    }
}

.catch()

catch(onRejected) {
    return this.then(undefined, onRejected);
}

.finally()

finally(onSettled) {
    return this.then(
        (data) => {
            onSettled(data);
            return data;
        },
        (reason) => {
            onSettled();
            throw reason;
        }
    );
}

.resolve() 和 .reject()

static resolve(data) {
    if (data instanceof MyPromise) {
        return data;
    }
    return new MyPromise((resolve, reject) => {
        if (isPromise(data)) {
            data.then(resolve, reject);
        } else {
            resolve(data);
        }
    });
}

static reject(reason) {
    return new MyPromise((resolve, reject) => {
        reject(reason);
    });
}

.all()

static all(params) {
    return new MyPromise((resolve, reject) => {
        try {
            const result = [];
            let count = 0;
            let fulfilledCount = 0;
            for (const iterator of params) {
                let i = count;
                count++;
                MyPromise.resolve(iterator).then((data) => {
                    fulfilledCount++;
                    result[i] = data;
                    if (fulfilledCount === count) {
                        resolve(result);
                    }
                }, reject);
            }
            if (count === 0) resolve(result);
        } catch (error) {
            reject(error);
        }
    });
}

.allsettled()

static allSettled(params) {
    const ps = [];
    for (const iterator of params) {
        ps.push(
            iterator.then(
                (value) => ({
                    status: FULFILLED,
                    value,
                }),
                (reason) => ({
                    status: REJECTED,
                    reason,
                })
            )
        );
    }
    return MyPromise.all(ps);
}

.race()

static race(params) {
    return new MyPromise((resolve, reject) => {
        for (const iterator of params) {
            MyPromise.resolve(iterator).then(resolve, reject);
        }
    });
}

通过以上实现,我们完成了一个简化版的 Promise,基本符合 Promise A+ 规范的核心内容。