前言
Promise
对于我们来说并不陌生,无论是在面试还是开发过程中,它都会频繁出现。虽然我们经常使用它,但如果能深入理解其底层原理,无疑能够提升我们的开发效率。因此,深入学习 Promise
是非常必要的。那么,接下来就让我们一起揭开 Promise
的神秘面纱,一起手写一个Pormise
吧!
在开始之前,我们以常见的Promise
面试题来理清接下来我们要做什么:
- Promise 解决了什么问题
- Promise 常用的 API 有哪些
- 实现 Promise 某个方法
- Promise 在事件循环中的执行过程是怎么样的
- Promise 的缺陷有哪些,可以怎么解决
这几个问题相信大家都不会感到陌生,而且在我们面试的时候,面试官大多都会像这几个问题一样循序循序渐进的来对我们进行提问,那么接下来我们就来一一解答吧!
出现原因
在Promise
出现以前,我们处理多个异步请求时,代码是这样的:
js
function sayHello(){
setTimeout(function () {
console.log(name);
}, 1000);
}
这是比较简单的,但是我们在有些场景中会遇到:第一次请求得到的结果是第二次请求的参数,一两次还好,但要是次数较多时就变成了这样:
js
function request1(callback) {
setTimeout(function() {
var result1 = 'data from request1';
callback(null, result1);
}, 1000);
}
function request2(param, callback) {
setTimeout(function() {
var result2 = 'data from request2 with param ' + param;
callback(null, result2);
}, 1000);
}
function request3(param, callback) {
setTimeout(function() {
var result3 = 'data from request3 with param ' + param;
callback(null, result3);
}, 1000);
}
request1(function(error, result1) {
if (error) {
console.error('Error in request1:', error);
} else {
request2(result1, function(error, result2) {
if (error) {
console.error('Error in request2:', error);
} else {
request3(result2, function(error, result3) {
if (error) {
console.error('Error in request3:', error);
} else {
console.log('Final result:', result3);
}
});
}
});
}
});
是不是看着就很头大,为了解决这样的问题,ES6提出了使用 Promise
来解决类似这样的 "回调地狱"
。 来看看Promise
实现是什么样的:
js
function request1() {
return new Promise((resolve, reject) => {
setTimeout(function() {
var result1 = 'data from request1';
resolve(result1);
}, 1000);
});
}
function request2(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
var result2 = 'data from request2 with param ' + param;
resolve(result2);
}, 1000);
});
}
function request3(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
var result3 = 'data from request3 with param ' + param;
resolve(result3);
}, 1000);
});
}
request1()
.then(result1 => request2(result1))
.then(result2 => request3(result2))
.then(finalResult => console.log('Final result:', finalResult))
.catch(error => console.error('Error:', error));
刚才臃肿的嵌套瞬间变得清爽了起来,真的nb!
让我们回到之前的问题,Promise
的出现是为了解决什么样的问题?看完上面的例子想必你已经有了答案:Promise
将嵌套调用改为了链式调用,使得代码的可读性和可维护性都大大的提高了。
开始实现
PS:业界所有的
Promise
都是遵循 Promise/A+规范,有兴趣的小伙伴可以去了解一下~另外,最后我们也会使用工具来测试我们的代码!
不太熟悉Promise
可以先去Promise mdn熟悉一下~
基础版
在我们通常的开发中经常会这样使用 Promise:
js
let promise = new Promise((resolve, reject) => {
console.log("promise resolve");
resolve("ok");
});
console.log('promise start')
promise.then(
(value) => {
console.log(value);
},
(error) => {
console.log(error);
}
);
控制台打印输出:
text
promise resolve
promise start
ok
在这个例子中,我们
- 首先创建一个新的
Promise
对象并立即执行。 - 构建
Promise
时传入一个executor
函数,Promise
的主要业务流程都在这个函数中。 - 如果运行在
executor
函数中的业务执行成功则调用resolve
,反之则调用reject
。 - 另外,
Promise
状态不可逆,一旦从Pending
无论是失败还是成功都无法回到pending
,也就是说resolve
和reject
同时调用时,默认会采取第一次调用的结果。 - 最后使用
then
方法处理Promise
的结果。
根据以上内容我们可以总结一下Promise
大致有以下特点:
- 有三个状态:
pending
、fulfilled
、rejected
; - 默认状态为
pending
; - 状态只能从
pending
到fulfilled
或从pending
到rejected
(如上图),状态一旦确认就不能再改变; new Promise
时会传入一个executor
执行器,执行器立即执行;executor
接受两个参数,分别是resolve
、reject
- 在成功时有一个保存成功状态的值(
value
),可以是undefined
、thenable
、promise
; - 在失败时有一个保存失败状态的值(
reason
); - 必须有
then
方法,then
接受两个参数:分别是成功的回调onFulfillment
、失败的回调onRejection
; - 如果调用
then
时,成功则执行onFulfilled
,参数为Promise
的value
; - 如果调用
then
时,失败则执行onRejcted
,参数为Promise
的reason
; - 如果
then
过程中抛出了异常,那么就将异常作为参数传递给下一个then
失败的回调onRejcted
;
接下来,我们将按照上面的特点勾勒出基础版的Promise
的形状:
js
// 三个状态-特点1
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class Promise {
// 接受一个执行器参数
constructor(executor) {
this.status = PENDING; // 默认状态-特点2
this.value = undefined; // 存放成功的值-特点6
this.reason = undefined; // 存放失败的值-特点7
let resolve = (value) => {
if (this.status === PENDING) {
// 改变状态-特点3
this.status = FULFILLED;
this.value = value;
}
};
let reject = (reason) => {
if (this.status === PENDING) {
// 改变状态-特点3
this.status = REJECTED;
this.reason = reason;
}
};
try {
// 立即执行且接受两个参数-特点4、5
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// then方法接受成功和失败的回调-特点8
then(onFulFilled, onRejected) {
// 成功-特点9
if (this.status === FULFILLED) {
onFulFilled(this.value);
}
// 失败-特点10
if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
根据Promise
的特点我们可以很轻松的勾勒出其大致的架构,但光勾勒出架构肯定是不够的,所以我们完善我们的代码,将其功能补充完整。
乍一看我们的我们代码写的有模有样的,这不得写点测试代码定位一下我们还有什么地方需要完善的吗?我们就拿上面的面试题测试一下吧
js
let promise = new Promise((resolve, reject) => {
console.log("promise resolve");
resolve("ok");
});
console.log("promise start");
promise.then(
(value) => {
console.log(value);
},
(error) => {
console.log(error);
}
);
// 输出
// promise resolve
// promise start
// ok
emmm,大致没啥问题,但是目前来说我们实现的版本仅限于同步操作的Promsie
,如果在executor
中传入一个异步操作的话就会发现没有返回内容,不信我们就将之前的测试代码改为:
js
let promise = new Promise((resolve, reject) => {
console.log("promise resolve");
setTimeout(() => {
resolve("ok");
}, 1000);
});
console.log("promise start");
promise.then(
(value) => {
console.log(value);
},
(error) => {
console.log(error);
}
);
// 输出
// promise resolve
// promise start
执行后我们会发现本该在 1s 后出现的 ok
不见了,这是为什么呢?明明我们都写得差不多的了。
这是因为现在Promise
在调用 then
方法时,当前的Promise
并没有成功,一直处于Pending
状态。所以如果当调用 then
方法的时,我们需要将成功或失败的回调分别使用不同的数组存放起来,在executor
的异步任务被执行时触发resolve
或reject
并依次调用成功或失败的回调。所以我们需要在executor
中分别添加存放成功和失败的回调的数组,接着在then
方法中判断当状态为pending
的时候将回调都push
到对应的数组中,最后在resolve
或在reject
中将数组中的回调依次执行。接下来我们将尝试先完成当成功(resolve
)时有异步操作时的情况并测试一下看看:
js
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
const PENDING = "PENDING";
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = []; // 存放成功回调的数组
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) => fn()); // 依次执行成功数组中的回调
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulFilled, onRejected) {
if (this.status === FULFILLED) {
onFulFilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
if (this.status === PENDING) {
// 状态为 pending 时将成功的回调存放至数组中
this.onResolvedCallbacks.push(() => {
onFulFilled(this.value);
});
}
}
}
我们还是拿上面的代码测试一下,打印结果:
text
promise resolve
promise start
ok
完美,看来我们已经完成了成功时的异步问题了,失败回调异步和成功回调异步处理方式一样的,我这里就不多阐述啦。
这里提一嘴:其实这里是一个发布订阅模式---
收集依赖--> 触发通知--> 取出依赖执行
。
链式调用&值的穿透
看完上面的内容后,我们知道Promise
使用了链式调用
的方式解决了以前回调地狱 的问题。我们在使用Promsie
的时候,当 then
方法中返回任何一个值,我们都可以在下一个then
方法中获取到,这也就是链式调用 。除此之外,当我们不在then
方法中放入参数:Promise.then().then()
,那么后面的then
仍然可以获取到之前then
放回的值,这就是值的穿透
。了解完Promise
的两大特性,我们一点一点的来捋清楚:每次调用then
方法的时候,都需要重新创建一个Promise
对象,同时把上一个then
返回的结果传递给这个新的Promise
的then
方法中,这样就使得then
方法一直传递下去了。
梳理一下我们将要做的事情:
then
方法的参数onFulfilled
和onRejectd
可以缺省, 如果onFulfilled
或onRejectd
不是函数则忽略,且依次在后面的then
中获取到之前返回的值;Promise
可以连续then
多次,每一次then
后都会返回一个新的promsie
;- 如果
then
返回的值是一个普通值,那么就将这个值传递给下一个then
或成功的回调; - 如果
then
返回的值是一个promise
,且该值同时调用resovle
和reject
,则优先第一次调用,剩余调用则忽略; - 如果
then
返回的值是一个promise
,那么就等待这个promise
执行完。如果成功则走下一个then
的成功,反之则走下一个then
的失败; - 如果
then
返回的值是和promise
是同一个引用对象,为避免造成循环引用 需要抛出异常,将异常传递给下一个then
失败的回调中; - 如果
then
中抛出了异常,那么就需要将其传递给下一个then
的失败回调;
按照以上特点,我们尝试将其完成:
js
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
const PENDING = "PENDING";
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulFilled, onRejected) {
// 判断`then`传递的值是否缺省-特点1
onFulFilled = typeof onFulFilled === "function" ? onFulFilled : (v) => v;
onRejected = typeof onRejected === "function" ? onRejected : (error) => { throw error }
// 每次`then`都会返回一个新的Promise-特点2
const newPromise = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
const x = onFulFilled(this.value);
this.#resolvePromise(newPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.#resolvePromise(newPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulFilled(this.value);
this.#resolvePromise(newPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.#resolvePromise(newPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return newPromise;
}
#resolvePromise(newPromise, x, resolve, reject) {
// 如果返回的新Promise和传递的值是同一个引用像会导致循环引用-特点6
if (newPromise === x) return reject(new TypeError("..."));
// 防止多次调用-特点4
let called;
// x 可能是一个 Promise-特点4
if ((typeof x === "object" && x !== null) || typeof x === "function") {
try {
let then = x.then;
// 如果`then`是一个函数说明 x 是 Promise-特点5
if (typeof then === "function") {
then.call(
x,
// 执行成功,将newResolve作为新promise的值-特点5
(newResolve) => {
if (called) return;
called = true;
//
this.#resolvePromise(newPromise, newResolve, resolve, reject);
},
// 执行失败,将newReject作为新promise的值-特点5
(newReject) => {
if (called) return;
called = true;
reject(newReject)
}
);
} else {
// x 是一个普通值-特点3
resolve(x);
}
} catch (error) {
// 对`then`中抛出的异常进行处理-特点7
reject(error);
}
} else {
// x 是一个普通值-特点3
resolve(x);
}
}
}
这里说明一下,为什么我们要在then
方法中添加setTimeout
:原生Promise
是 V8 (感兴趣的小伙伴可以前往查看哈) 引擎提供的微任务,这里使用setTimeout
来模拟异步 ,确保onFulfilled
和onRejected
函数在执行环境的栈为空时才被调用,但是这里是宏任务
。setTimeout
会将它的回调函数放入到一个任务队列中,只有当当前的执行栈为空时,才会从队列中取出回调函数执行。PromiseA+
规范也提到:这可以通过"宏任务"机制(例如 setTimeout
或 setImmediate
)或"微任务"机制(例如MutatonObserver
)来实现process.nextTick
。
完成了then
方法,接下来我们将其进行测试一下:
js
const promise = new Promise((resolve, reject) => {
// reject("失败");
resolve("成功");
})
.then()
.then()
.then(
(data) => {
console.log("ok", data);
},
(err) => {
console.log("err", err);
}
);
// 输出
// ok 成功
到目前为止,我们已经完成了Promise
的大部分内容,剩下de Promise API
在我们完成这个后已经是小打小闹了。既然开头我们提到了业界的大部分Promise
类库都遵循PromiseA+
标准,那么我们接下来使用测试工具测试一下。
测试
PromiseA+
的测试工具名为promises-aplus-tests
,我们先在我们的代码中添加如下代码:
js
// just for promise test
Promise.defer = Promise.deferred = function () {
let dtd = {};
dtd.promise = new Promise((resolve, reject) => {
dtd.resolve = resolve;
dtd.reject = reject;
});
return dtd;
};
module.exports = Promise;
然后在终端中执行下面的命令
js
npm init -y
npm i promises-aplus-tests
npx promises-aplus-tests 你的文件名
promises-aplus-tests 中共有 872 条测试用例。以上代码,可以完美通过所有用例😏
Promise API
实现promise
肯定不能忘记实现其API呀,我们知道原生Promise
提供了以下方法:
- Promise.resolve
- Promise.reject
- Promise.prototype.catch
- Promise.prototype.finally
- Promise.all
- Promise.race
- Promise.allSettled
接下来我们一一进行实现。
Promise.resolve
- 默认产生一个成功的
Promise
; - 具备等待功能。如果参数是
Promise
则会等待其解析完成后才会向下执行,所以这里还需要在resolve
中处理一下;
js
// 处理resolve
let resolve = (value) => {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
resolve 方法:
js
static resolve(value) {
return new Promise((resolve) => {
resolve(value)
});
}
测试一下~
js
const promise1 = Promise.resolve("Hello, World!");
promise1.then((value) => {
console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
setTimeout(() => {
console.log(value);
}, 3000);
});
const promise3 = Promise.resolve(promise1);
promise3.then((value) => {
console.log(value);
});
// 输出
// Hello, World!
// Hello, World!
// 3s后 - 42
Promise.reject
- 默认产生一个失败的
Promise
;
js
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
Promise.prototype.catch
- 捕获
Promise
中出现的异常,即没有成功的then
;
js
catch(errorCallback) {
return this.then(null, errorCallback);
}
Promise.prototype.finally
- 无论如何都会执行的内容。如果返回
Promise
则会等待其执行完毕,如果返回成功的Promise
会采用上一次的结果,如果返回失败的Promise
,会用这个失败的结果传递到catch
中。
js
finally(callback) {
return this.then(
(value) => {
return Promise.resolve(callback()).then(() => value);
},
(reason) => {
return Promise.resolve(callback()).then(() => {
throw reason;
});
}
);
}
测试一下:
js
Promise.resolve(456)
.finally(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(123);
reject('Error')
}, 3000);
});
})
.then((data) => {
console.log(data, "success");
})
.catch((err) => {
console.log(err, "error");
});
// 输出
// Error error
Promise.all
常用来处理并发请求。
- 接收一个
Promise
数组作为参数; - 返回成功的
Promise
; - 只要有一个失败则失败;
js
static all(promises) {
if (!Array.isArray(promises)) return new TypeError("...");
return new Promise((resolve, reject) => {
const res = [];
let counter = 0;
const processResultByKey = (value, index) => {
res[index] = value;
counter++;
if (counter === promises.length) return resolve(res);
};
for (let i = 0; i < promises.length; i++) {
if (promises[i] && typeof promises[i].then === "function") {
promises[i].then((value) => {
processResultByKey(value, i);
}, reject);
} else {
processResultByKey(promises[i], i);
}
}
});
}
测试一下:
js
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
setTimeout(() => {
console.log(value);
}, 3000);
});
Promise.all([1, 2, 3, promise1, promise2]).then(
(data) => {
console.log("resolve", data);
},
(err) => {
console.log("reject", err);
}
);
// 输出:
// success!
// resolve [ 1, 2, 3, 'success!', 42 ]
// 3s后-42
Promise.race
Promise.race
被用来处理多个请求,但只返回最快的那个
- 接受一个
Promise
数组; - 返回最快的那一个;
- 有一个成功则成功;
js
static race(promises) {
if (!Array.isArray(promises)) return new TypeError("...");
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
if (promises[i] && typeof promises[i].then === "function") {
promises[i].then(resolve, reject);
} else {
resolve(promises[i]);
}
}
});
}
测试一下:
js
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
setTimeout(() => {
console.log(value);
}, 3000);
});
Promise.race([1, 2, 3, promise1, promise2]).then(
(data) => {
console.log("resolve", data);
},
(err) => {
console.log("reject", err);
}
);
// 输出
// success!
// resolve 1
// 42
Promise.allSettled
- 接受一个
Promise
数组; - 无论成功或者失败都会返回;
- 每一个对象都会有一个记录其状态的兑现;
js
static allSettled(promises) {
if (!Array.isArray(promises)) return new TypeError("...");
return new Promise((resolve, reject) => {
const res = [];
let counter = 0;
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then((value) => {
res[i] = { status: FULFILLED, value };
})
.catch((reason) => {
res[i] = { status: REJECTED, reason };
})
.finally(() => {
counter++;
if (counter === promises.length) resolve(res);
});
}
});
}
测试一下:
js
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
setTimeout(() => {
console.log(value);
}, 3000);
});
Promise.allSettled([1, 2, 3, promise1, promise2]).then(
(data) => {
console.log("resolve", data);
},
(err) => {
console.log("reject", err);
}
);
// 输出
// success!
// resolve [
// { status: 'FULFILLED', value: 1 },
// { status: 'FULFILLED', value: 2 },
// { status: 'FULFILLED', value: 3 },
// { status: 'FULFILLED', value: 'success!' },
// { status: 'FULFILLED', value: 42 }
// ]
// 3s后-42
完美!
总结
在本文中,我们深入探讨了 Promise 及其相关 API 的实现。我们首先了解了 Promise 的基本概念和工作原理,然后我们实现了一个简单的 Promise
及其相关的API。通过这个过程,我们可以更深入地理解 Promise
的工作机制,以及 JavaScript
的异步编程模型。总的来说,手写 Promise
及其相关 API 是一个很好的练习,它可以帮助我们更好地理解Promise
。但在实际的项目中,我们通常会直接使用 JavaScript 提供的原生
Promise`,因为它已经经过了严格的测试,可以确保在各种情况下都能正常工作。
希望这篇文章能帮助你更好地理解 Promise
,以及如何在你的代码中使用它。如果你有任何问题或者想要深入探讨某个话题,欢迎留言讨论。