该系列是本人准备面试的笔记,或许有描述不当的地方,请在评论区指出,感激不尽。
其他篇章:
- 从 babel 编译看 async/await
- 挑战ChatGPT提供的全网最复杂“事件循环”面试题
- Vue.nextTick 从v3.5.13追溯到v0.7.0
- Vue 怎么监听 Set,WeakSet,Map,WeakMap 变化?
- Vue 是怎么从<HelloWorld />、<component is='HelloWorld'>找到HelloWorld.vue
前言
最近在准备面试,刚好看到金石计划,所以一拍即合。准备写一系列文章巩固知识点的同时,顺便赚点零花钱,一举两得。
本篇是 Promise 的相关总结,走过路过不要错过,点赞收藏评论,一键三连 [比心]。
Promise 的诞生
众所周知,JavaScript 是一种单线程的语言,依赖事件循环机制来实现异步操作。常见的异步任务包括:
- 网络请求(如
fetch
) - 文件操作(如
fs
模块) - 定时器(如
setTimeout
)
然而,传统的异步编程模式主要依赖 回调函数(callback),这种模式虽然简单,但在实际应用中暴露出了一些严重的问题。
1. 回调地狱
当多个异步操作需要串行执行时,回调函数会层层嵌套,导致代码可读性极差。代码逻辑简单,却难以阅读和维护。
注:图片来自es6-深入理解promise,读书人的事,能算偷么?
2. 错误处理复杂
在传统的回调模式中,每个异步任务都需要独立的错误处理机制。稍有不慎,错误就可能被遗漏。
doSomething((err, result) => {
if (err) {
handleError(err);
} else {
doAnotherThing(result, (err, anotherResult) => {
if (err) {
handleError(err);
}
});
}
});
3. 缺乏流程控制能力
回调函数没有提供内置的工具来管理多个异步任务的组合,比如并行处理多个任务并获取其结果。
Promise 的出现解决了这些问题:
- 它提供了一种更清晰的链式调用方式,减少嵌套。
- 它内置了错误捕获机制,使得异常处理更方便。
- 它是状态化的(Pending、Fulfilled、Rejected),避免了重复调用回调的问题。
Promise 的定义以及 Promises/A+ 规范
一个
Promise
是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。引自MDN
核心思想:
- 将异步操作的结果通过对象的 状态 表达(Pending、Fulfilled、Rejected),同时状态不可逆,一旦状态从
Pending
转变为Fulfilled
或Rejected
,就不可再更改。 - 支持链式调用,使代码扁平化,摆脱回调地狱。
- 通过
.catch
方法统一捕获错误。
Promises/A+ 规范
对 Promise 行为的标准化定义,目的是为不同的 Promise 实现提供一致性。它规范了 Promise 的核心行为,确保各种库或框架的 Promise 实现能够互相兼容。
核心要点:
- Promise 必须处于以下三种状态之一:Pending、Fulfilled、Rejected。
- Promise 必须提供
then
方法来访问其当前或最终的value
或reason
,该方法接受两个回调函数,一个onFulfilled
用于 Promise 被兑现时调用,一个onRejected
用于 Promise 被拒绝时调用。 - 如果
onFulfilled
或onRejected
返回了一个值x
,Promise 需要解析x
的值以决定新的 Promise 的状态。
promises-tests: Promises/A+ 规范 的官方测试套件,用于验证一个 Promise 实现是否完全遵循 Promises/A+ 标准。
Promise 相关 API
Promise.resolve
和 Promise.reject
就不过多介绍了。
Promise.all
Promise.all
接受一个包含多个 Promise 的数组,并返回一个新的 Promise。只有当数组中的所有 Promise 都成功时,Promise.all
才会变为 fulfilled
并返回所有结果的数组。如果其中一个 Promise 被拒绝,Promise.all
将立即变为 rejected
并返回第一个被拒绝的原因。
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
}).catch(reject); // 一旦有一个 Promise 被拒绝,直接 reject
});
});
};
- 批量 API 请求: 获取用户数据和对应的订单数据,然后一起处理。
- 加载多个资源: 并行加载多张图片或文件,等所有资源加载完成后再渲染页面。
Promise.allSettled
Promise.allSettled
接受一个包含多个 Promise 的数组,并返回一个新的 Promise,其状态总是 fulfilled
。返回的值是每个输入 Promise 的结果对象,包含 status
和 value
或 reason
。
Promise.allSettled = function(promises) {
return new Promise(resolve => {
let results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
results[index] = { status: 'fulfilled', value };
}).catch(reason => {
results[index] = { status: 'rejected', reason };
}).finally(() => {
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
});
});
});
};
- 批量任务监控: 执行多个任务(如文件上传),无论成功还是失败,都需要统计结果。
- 日志记录: 记录多个异步事件的执行结果,用于后续分析。
Promise.any
Promise.any
接受一个包含多个 Promise 的数组,返回一个新的 Promise。只要有一个 Promise 成功,Promise.any
就会变为 fulfilled
,并返回成功的值。如果所有 Promise 都被拒绝,则返回一个 AggregateError
。
Promise.any = function(promises) {
return new Promise((resolve, reject) => {
let rejections = [];
let rejectedCount = 0;
promises.forEach(promise => {
Promise.resolve(promise).then(resolve).catch(reason => {
rejections.push(reason);
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(rejections, 'All Promises were rejected'));
}
});
});
});
};
- 容灾处理: 从多个服务器中请求数据,只要一个服务器返回成功即可。
- 抢占式策略: 执行多个备用任务,最先成功的即为最终结果。
Promise.race
Promise.race
接受一个包含多个 Promise 的数组,返回一个新的 Promise。一旦其中任何一个 Promise 解决或拒绝,Promise.race
就会解决或拒绝。
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
Promise.resolve(promise).then(resolve).catch(reject);
});
});
};
- 请求超时: 设置一个超时机制,如果 API 请求超时则直接失败。
- 竞态任务: 两个任务同时执行,选最快完成的结果。
Promise.try
Promise.try
是一个非标准方法,用于确保某段代码以 Promise 的形式运行(不论是否同步)。这是为了统一处理同步和异步逻辑。
Promise.try = function(fn) {
return new Promise((resolve, reject) => {
try {
resolve(fn());
} catch (error) {
reject(error);
}
});
};
- 安全调用未知函数: 不确定函数是同步还是异步。
- 简化控制流: 使用
try-catch
的同时支持异步逻辑。
示例:
function mayBeSyncOrAsync() {
if (Math.random() > 0.5) {
return 'Sync value';
} else {
return Promise.resolve('Async value');
}
}
Promise.try(() => mayBeSyncOrAsync())
.then(result => console.log('Result:', result))
.catch(error => console.error('Error:', error));
Promise.withResolvers
Promise.withResolvers
是一个实用工具,返回一个对象,包含一个 Promise 和该 Promise 的 resolve
和 reject
方法,方便控制 Promise
的状态。
Promise.withResolvers = function() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
- 事件驱动模型: 在事件发生时触发 Promise 完成。
- 复杂的异步流程: 手动挂起和恢复任务。
示例:
function asyncOperation() {
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Operation succeeded');
} else {
reject(new Error('Operation failed'));
}
}, 1000);
return promise;
}
asyncOperation()
.then(result => console.log(result))
.catch(error => console.error(error));
原理实现
Don't say too much,面试题常客,直接上代码。
class MyPromise {
constructor(executor) {
// 初始化状态
this.state = "pending"; // 状态:pending、fulfilled、rejected
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
// 成功的回调
const resolve = (value) => {
if (this.state === "pending") {
setTimeout(() => { // 确保是异步执行
this.state = "fulfilled";
this.value = value;
this.onFulfilledCallbacks.forEach((callback) =>
callback(value)
);
});
}
};
// 失败的回调
const reject = (reason) => {
if (this.state === "pending") {
setTimeout(() => {
this.state = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach((callback) =>
callback(reason)
);
});
}
};
// 捕获 executor 的异常
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 支持值穿透
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
onRejected =
typeof onRejected === "function"
? onRejected
: (e) => {
throw e;
};
// 返回新的 Promise 实现链式调用
return new MyPromise((resolve, reject) => {
const fulfilledTask = () => {
try {
const x = onFulfilled(this.value);
resolvePromise(x, resolve, reject);
} catch (error) {
reject(error);
}
};
const rejectedTask = () => {
try {
const x = onRejected(this.reason);
resolvePromise(x, resolve, reject);
} catch (error) {
reject(error);
}
};
if (this.state === "fulfilled") {
setTimeout(fulfilledTask);
} else if (this.state === "rejected") {
setTimeout(rejectedTask);
} else if (this.state === "pending") {
this.onFulfilledCallbacks.push(fulfilledTask);
this.onRejectedCallbacks.push(rejectedTask);
}
});
}
}
// 处理 then 的返回值 x
function resolvePromise(x, resolve, reject) {
if (x === resolve) {
return reject(new TypeError("Circular reference detected"));
}
if (x instanceof MyPromise) {
// 如果返回的是另一个 Promise,则等待其状态改变
x.then(resolve, reject);
} else {
resolve(x);
}
}
有趣的 Promise 相关项目
以前觉得 Promise 不就 then
,catch
,finally
,有什么难的。直到看见 那个男人 的仓库,才知道 Promise 的水很深,可玩的东西太多了。
以下列举一些我认为有趣的项目:
-
bluebird:一个高性能、功能丰富的 JavaScript Promise 库。它提供了比原生 JavaScript Promises 更多的功能,同时具有极高的性能表现。
- 提供了许多额外的静态方法和实例方法,例如
.map
,.reduce
,.filter
,.each
,方便进行异步操作的组合和处理。 - 内置的
.cancel()
方法,用于支持可取消的 Promises。 .timeout()
方法,用于为异步操作设置超时时间。
- 提供了许多额外的静态方法和实例方法,例如
-
q:另一个流行的 JavaScript Promise 库,它是最早实现并推广 Promises/A+ 规范的库之一。
defer()
用于创建和控制 Deferred 对象。- 提供
Q.nfcall
和Q.denodeify
,用于将基于 Node.js 回调风格的函数转换为 Promise 风格
-
p-queue:适用于限制异步(或同步)操作的速率。例如,与 REST API 交互时或执行 CPU/内存密集型任务时。
p-queue
允许你设置并发限制(比如最大同时执行的异步任务数),确保你的应用不会超负荷执行任务。可以在启动队列时指定concurrency
参数,来限制队列中同时执行的任务数。- 当并发任务数量达到上限时,
p-queue
会将新任务排入队列,直到有足够的空间可以执行它们。这样可以避免任务堆积导致性能瓶颈。 - 提供了优先级队列的功能,允许你为不同的任务设置不同的优先级,使得高优先级的任务先执行。
- 支持取消已加入队列的任务(如果任务还没有执行)。
-
promisees:为开发者提供关于 Promise 性能的可视化和统计数据,帮助识别可能存在的性能瓶颈或不良的异步操作实践。
-
p-progress:对于在长时间运行的异步操作期间向用户报告进度。
-
proxymise:允许方法和属性链式操作,无需中间的
then()
或await
,从而获得更简洁的代码。
const proxymise = require('proxymise');
// Instead of thens
foo.then(value => value.bar())
.then(value => value.baz())
.then(value => value.qux)
.then(value => console.log(value));
// Instead of awaits
const value1 = await foo;
const value2 = await value1.bar();
const value3 = await value2.baz();
const value4 = await value3.qux;
console.log(value4);
// Use proxymise
const value = await proxymise(foo).bar().baz().qux;
console.log(value);
* 还有 promise-fun 记录了 sindresorhus 所有 Prumise 相关的模块。
最后
希望这篇文章能带给你新的感悟。说实话,我也是复习的时候才知道 Promise.try
和 Promise.withResolvers
。
朋友们,你们遇到的 Promise 面试真题都是什么样子?欢迎在评论区分享。