一、回调地狱与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 引入了 async 和 await 关键字。
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 函数中使用。
通过 async 和 await,我们可以以同步的方式编写异步代码,使代码更加简洁易读。
七、手写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 函数接收两个参数 onFulfilled 和 onRejected,并返回一个新的 Promise。then 函数中的回调函数会在微任务队列中执行:
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+ 规范的核心内容。