主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green
贡献主题:github.com/xitu/juejin…
theme: fancy highlight:
Promise已经成为现代前端异步编程的基础。很多新的API和异步编程方式都建立在Promise基础上。因此Promise也是前端开发者必须掌握的技能。
为什么需要Promise?
在没有Promise之前,我们一般通过回调函数进行异步操作,而一旦异步操作依赖嵌套过多,就会出现恶心的回调地狱:
// fetchAPI是mocked请求API方法
fetchAPI("/target-a", (a) => {
fetchAPI(`/target-b?a=${a}`, (b) => {
fetchAPI(`/target-c?b=${b}`, (c) => {
fetchAPI(`/target-d?c=${c}`, (d) => {
console.log("result", d);
});
});
});
});
除此之外,回调函数还存在信任问题,我们只能把自己的回调函数传给类似fetchAPI
这样第三方函数,回调函数的触发时机和触发次数不受我们自己控制。
Promise的出现很好的解决了这些问题。Promise通过链式调用解决了嵌套回调;通过控制反转解决信任问题。如果fetchAPI
是基于Promise实现的,上面的代码可以变成:
fetchAPI("/target-a")
.then((a) => fetchAPI(`/target-b?a=${a}`))
.then((b) => fetchAPI(`/target-c?b=${b}`))
.then((c) => fetchAPI(`/target-d?c=${c}`))
.then((d) => {
console.log("result", d);
});
从零实现Promise
如果你对Promise的使用还不熟悉,建议先查看:阮一峰的Promise教程,以及Promise/A+规范。
本文相关代码均在toy-promise仓库。
我们先来看一个简单的Promise例子:
console.log("start");
const p1 = new Promise((resolve, reject) => {
resolve("data");
});
p1.then((data) => {
console.log("result:", data);
});
console.log("end");
输出结果为:
start
end
result: data
根据Promise/A+规范,Promise的基本特征是:
-
构造Promise对象时,需要一个executor执行函数,该函数接受两个参数,分别是
resolve
和reject
,并且在构建时立即执行 -
Promise有三种状态:
pending
,fulfilled
,rejected
;Promise的默认状态是pending
,且Promise的状态只能是从pending
到fulfilled
,或者pending
到rejected
,一旦状态改变确认,就不会再发生改变 -
Promise使用
value
保存成功时的结果,使用reason
保存失败时的结果 -
Promise必须具有
then
方法,该方法接受两个参数,分别是成功时执行的onFulfilled
回调,以及失败时执行的onRejected
回调; -
调用
then
时,如果Promise已经成功,则执行onFulfilled
回调,其参数是value
;如果Promise已经失败,则执行onRejected
回调,其参数是reason
根据这些规则,我们可以先试着构建一个最基础的版本:
// Promise 三种状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class ToyPromise {
constructor(executor) {
// Promise默认状态是pending
this.status = PENDING;
// 用于存放成功的值
this.value = undefined;
// 用于存放失败的原因
this.reason = undefined;
try {
// 立即执行executor
executor(this.resolve, this.reject);
} catch (error) {
// 如果执行executor发生错误,这把该Promise置为失败
this.reject(error);
}
}
// resolve函数需要记住this值,所以使用箭头函数
resolve = (value) => {
// 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
if (this.status !== PENDING) {
return;
}
this.status = FULFILLED;
this.value = value;
};
// reject函数需要记住this值,所以使用箭头函数
reject = (reason) => {
// 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用reject方法
if (this.status !== PENDING) {
return;
}
this.status = REJECTED;
this.reason = reason;
};
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
} else if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
resolve("mock data");
});
p1.then(
(data) => console.log("success", data),
(error) => console.log("fail", error)
);
const p2 = new ToyPromise((resolve, reject) => {
reject("error");
});
p2.then(
(data) => console.log("success", data),
(error) => console.log("fail", error)
);
输出结果为:
"success mock data"
"fail error"
到现在为止,同步版Promise已经实现了,但现在的版本还不支持异步操作:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("mock data");
}, 1000);
});
p1.then(
(data) => console.log("success", data),
(error) => console.log("fail", error)
);
上面的代码并不会输出任何结果,因为在执行then
方法的时候,Promise的状态还是pending
,自然不会执行任何回调。
所以,解决思路就是在调用then
方法时,如果当前状态是pending
,那我们需要先把回调函数存起来,等到Promise状态改变后,再执行相应的回调函数,根据这个思路再继续优化代码:
class ToyPromise {
constructor(executor) {
// Promise默认状态是pending
this.status = PENDING;
// 用于存放成功的值
this.value = undefined;
// 用于存放成功的回调
this.onFulfilledQueue = [];
// 用于存放失败的原因
this.reason = undefined;
// 用于存放失败的回调
this.onRejectedQueue = [];
try {
// 立即执行executor
executor(this.resolve, this.reject);
} catch (error) {
// 如果执行executor发生错误,这把该Promise置为失败
this.reject(error);
}
}
// resolve函数需要记住this值,所以使用箭头函数
resolve = (value) => {
// 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
if (this.status !== PENDING) {
return;
}
this.status = FULFILLED;
this.value = value;
// 依次执行相应的回调
this.onFulfilledQueue.forEach((fn) => fn(value));
};
// reject函数需要记住this值,所以使用箭头函数
reject = (reason) => {
// 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用reject方法
if (this.status !== PENDING) {
return;
}
this.status = REJECTED;
this.reason = reason;
// 依次执行相应的回调
this.onRejectedQueue.forEach((fn) => fn(reason));
};
then(onFulfilled, onRejected) {
// 如果状态是pending,则先将两个回调存起来
if (this.status === PENDING) {
this.onFulfilledQueue.push(onFulfilled);
this.onRejectedQueue.push(onRejected);
} else if (this.status === FULFILLED) {
onFulfilled(this.value);
} else {
onRejected(this.reason);
}
}
}
再用之前的异步代码测试一下,控制台将在1s后输出"success mock data"
。
then链式调用
前面提到,Promise通过链式调用消除了回调地狱,当我们使用了then
方法后,还可以继续调用then
方法。在Promise/A+规范中规定:
- Promise的
then
方法可以被同一个Promise多次调用,且每次都会返回一个新的Promise对象:p2=p1.then(onFulfilled,onRejected)
- 如果
then
方法中的回调函数onFulfilled
或者onRejected
执行时抛出异常,那么新的Promise会失败,并且把这个异常作为失败的reason
- 如果
then
方法中的回调函数onFulfilled
或者onRejected
执行时返回结果x
- 如果
x
是Promise,或者thenable
对象- 如果
x
和新的Promise引用相同,则抛出TypeError
- 否则新Promise的最终结果由该对象完成后的状态决定,如果该对象最终执行成功,则新的Promise也成功,如果该对象最终执行失败或者执行过程中抛出异常,则新的Promise也失败
- 如果
- 如果
x
是其他普通值,那么新的Promise也成功,且成功的value
为x
- 如果
我们继续来完善then
方法:
then(onFulfilled, onRejected) {
// 返回一个新的Promise
const p = new ToyPromise((resolve, reject) => {
// 如果状态是pending,则先将两个回调存起来
if (this.status === PENDING) {
this.onFulfilledQueue.push((value) => {
setTimeout(() => {
try {
const x = onFulfilled(value);
// x可能是一个Promise或者thenable对象
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedQueue.push((reason) => {
setTimeout(() => {
try {
const x = onRejected(reason);
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
} else if (this.status === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
}
});
return p;
}
}
function resolvePromise(promise, x, resolve, reject) {
// 如果promise和x是同一个对象,那么就会出现自己等待自己完成的问题
if (promise === x) {
return reject(TypeError("Chaining cycle detected"));
}
// 规范中规定resolve和reject只能调用一次,使用called作为是否promise已经完成的标识
// 虽然多次调用也没有问题
let called = false;
if ((x && typeof x === "object") || typeof x === "function") {
let then;
try {
//将这句代码放入try/catch中,用于处理对象的设置了then的get操作符直接抛出错误的情况
then = x.then;
if (typeof then === "function") {
then.call(
x,
(value) => {
if (called) {
return;
}
called = true;
// 继续递归解析
resolvePromise(promise, value, resolve, reject);
},
(reason) => {
if (called) {
return;
}
called = true;
reject(reason);
}
);
} else {
if (called) {
return;
}
called = true;
// 如果then不是一个函数,则直接resolve
resolve(x);
}
} catch (error) {
if (called) {
return;
}
called = true;
reject(error);
}
} else {
if (called) {
return;
}
called = true;
// 如果x是一个普通值,则直接resolve
resolve(x);
}
}
在Promise/A+规范中提到,onFulfilled
和onRejected
回调函数需要异步执行,原生Promise是通过创建一个微任务异步执行回调;我们这里则是通过setTimeout
生成一个宏任务执行回调。
then值穿透
Promise的then
方法还可以不传入参数,如promise.then().then().then(value=>{})
,后续的then
依旧可以得到前面的结果。这就是所谓的值穿透,要做到这一点很简单,我们只需要在then
方法中增加对回调函数的判断处理即可:
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
// ......
}
到此为止,我们就完成了Promise最核心的部分。写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("mock data");
}, 1000);
});
p1.then()
.then()
.then((value) => {
console.log("success", value);
});
控制台将在1s后输出"success mock data"
。
Resolve Promise
这里补充一点规范中没有提及的内容,如果执行executor
函数时,给resolve
回调传递的参数也是一个Promise或者thenable
的对象,那么Promise的结果由该对象完成后的状态决定。处理方式和then
方法中回调返回值是Promise或者thenable
的对象时处理方法一致。因此我们可以复用之前的resolvePromise
函数:
class ToyPromise {
// resolve函数需要记住this值,所以使用箭头函数
resolve = (value) => {
// 此时第一个参数直接赋值为undefined即可
resolvePromise(
undefined,
value,
(x) => {
// 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
if (this.status !== PENDING) {
return;
}
this.status = FULFILLED;
this.value = x;
// 依次执行相应的回调
this.onFulfilledQueue.forEach((fn) => fn(x));
},
this.reject
);
};
// ......
};
function resolvePromise(promise, x, resolve, reject) {
// 如果promise和x是同一个对象,那么就会出现自己等待自己完成的问题
// 如果是通过resolve调用,promise为undefined,需要将这种情况排除
if (promise && promise === x) {
return reject(TypeError("Chaining cycle detected"));
}
// ......
}
写点代码测试一下:
const p = new ToyPromise((resolve) => {
resolve(
new ToyPromise((resolve) => {
resolve({
then: (resolve) => {
resolve("data");
},
});
})
);
});
p.then(
(value) => console.log(value),
(error) => console.warn(error)
);
控制台将输出"data"
。
继续完善Promise
catch
catch
方法可以用来捕获Promise抛出的异常,它本身就是一个then
的语法糖:
catch(onRejected) {
return this.then(undefined, onRejected);
}
写点代码测试一下:
new ToyPromise((resolve, reject) => {
reject("error");
}).catch((error) => console.warn(error));
控制台将输出"error"
。
finally
finally
方法不管在Promise对象最后是什么状态都会执行,且finally
的回调函数不接受任何参数。只要finally
的回调函数本身不抛出异常或者返回一个失败的Promise,那么finally
方法总是会返回和之前Promise相同的结果,本质上也是then
的语法糖:
finally(onResolved) {
return this.then(
(value) => ToyPromise.resolve(onResolved()).then(() => value),
(reason) =>
ToyPromise.resolve(onResolved()).then(() => {
throw reason;
})
);
}
写点代码测试一下:
ToyPromise.resolve("success")
.finally(() => {
console.log("finally");
})
.then((value) => {
console.log(value);
});
ToyPromise.resolve("success")
.finally(() => {
return ToyPromise.resolve("data");
})
.then((value) => {
console.log(value);
});
ToyPromise.resolve("success")
.finally(() => {
return ToyPromise.reject("error");
})
.catch((error) => {
console.log(error);
});
控制台将输出:
"finally"
"success"
"success"
"error"
静态方法resolve
静态方法resolve
可以用于将一个任意类型的对象转换为Promise对象:
static resolve(value) {
return new ToyPromise((resolve) => {
resolve(value);
});
}
写点代码测试一下:
ToyPromise.resolve("a").then((value) => {
console.log(value);
});
ToyPromise.resolve(
new ToyPromise((resolve) => {
setTimeout(() => {
resolve("b");
}, 1000);
})
).then((value) => {
console.log(value);
});
控制台将首先输出"a"
,然后1s后输出"b"
。
静态方法reject
静态方法reject
可以用于生成一个失败的Promise,reject
方法并不关心所传递的对象是不是Promise或者thenable
对象:
static reject(reason) {
return new ToyPromise((resolve, reject) => {
reject(reason);
});
}
写点代码测试一下:
ToyPromise.reject("a").catch((error) => {
console.log(value);
});
const p = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("b");
}, 1000);
});
ToyPromise.reject(p).catch((error) => {
console.log(error);
});
控制台将首先输出"a"
,然后1s后输出Promise对象p
。
静态方法all
all
方法可以用于将多个Promise实例包装成为一个新的Promise,并且只有所有Promise成功时,新的Promise才成功,否则任意Promise失败,新的Promise即为失败:
static all(promises) {
if (!Array.isArray(promises)) {
return TypeError(`TypeError: ${promises} is not array`);
}
return new ToyPromise((resolve, reject) => {
const result = [];
let resolvedCount = 0;
// 空数组判断,防止Promise永远不结束
if (promises.length === 0) {
resolve(result);
}
promises.forEach((promise, idx) => {
// 将所有对象首先转换为Promise再统一处理
ToyPromise.resolve(promise).then((value) => {
result[idx] = value;
if (++resolvedCount === promises.length) {
resolve(result);
}
}, reject);
});
});
}
写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p1");
}, 1000);
});
const p2 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p2");
}, 2000);
});
const p3 = ToyPromise.resolve("p3");
ToyPromise.all([p1, p2, p3, "p4"]).then((result) => {
console.log(result);
});
控制台将在2s后输出[ "p1", "p2", "p3", "p4" ]
。
静态方法race
race
方法同样用于将多个Promise实例包装成为一个新的Promise,但新的Promise状态由最快完成的Promise决定:
static race(promises) {
if (!Array.isArray(promises)) {
return TypeError(`TypeError: ${promises} is not array`);
}
return new ToyPromise((resolve, reject) => {
if (promises.length === 0) {
resolve(undefined);
}
promises.forEach((promise) => {
ToyPromise.resolve(promise).then(resolve, reject);
});
});
}
写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p1");
}, 1000);
});
const p2 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p2");
}, 2000);
});
ToyPromise.race([p1, p2]).then((result) => {
console.log(result);
});
控制台将在1s后输出"p1"
。
静态方法allSettled
该方法由ES2020引入
allSettled
方法同样用于将多个Promise实例包装成为一个新的Promise,新的Promise会等到所有Promise都完成后(无论成功或失败)才完成,且完成状态总为成功:
static allSettled(promises) {
if (!Array.isArray(promises)) {
return TypeError(`TypeError: ${promises} is not array`);
}
return new ToyPromise((resolve, reject) => {
const result = [];
let resolvedCount = 0;
const saveResult = (data, idx) => {
result[idx] = data;
if (++resolvedCount === promises.length) {
resolve(result);
}
};
if (promises.length === 0) {
resolve(result);
}
promises.forEach((promise, idx) => {
ToyPromise.resolve(promise).then(
(value) => {
saveResult(value, idx);
},
(reason) => {
saveResult(reason, idx);
}
);
});
});
}
写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p1");
}, 1000);
});
const p2 = new ToyPromise((resolve, reject) => {
setTimeout(() => {
reject("error");
}, 2000);
});
ToyPromise.allSettled([p1, p2]).then((result) => {
console.log(result);
});
控制台将在2s后输出[ "p1", "error" ]
。
静态方法any
该方法由ES2021引入
any
方法同样用于将多个Promise实例包装成为一个新的Promise,它的处理方式和all
刚好相反,只有所有Promise失败时,新的Promise才失败,否则任意Promise成功,新的Promise即为成功:
static any(promises) {
if (!Array.isArray(promises)) {
return TypeError(`TypeError: ${promises} is not array`);
}
return new ToyPromise((resolve, reject) => {
const result = [];
let rejectedCount = 0;
if (promises.length === 0) {
resolve(undefined);
}
promises.forEach((promise, idx) => {
// 将所有对象首先转换为Promise再统一处理
ToyPromise.resolve(promise).then(resolve, (reason) => {
result[idx] = reason;
if (++rejectedCount === promises.length) {
reject(result);
}
});
});
});
}
写点代码测试一下:
const p1 = new ToyPromise((resolve) => {
setTimeout(() => {
resolve("p1");
}, 1000);
});
const p2 = new ToyPromise((resolve, reject) => {
setTimeout(() => {
reject("error");
}, 2000);
});
ToyPromise.any([p1, p2]).then((result) => {
console.log(result);
});
控制台将在1s后输出"p1"
。
测试Promise
我们可以通过promises-aplus-tests这个库来测试我们自己写的Promise是否符合规范。该库需要Promise暴露一个deferred
静态方法,为了不影响原本代码,我们单独声明一个Adapter文件:
class Adapter extends ToyPromise {
static deferred() {
const result = {};
result.promise = new Adapter((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
}
module.exports = Adapter;
执行命令promises-aplus-tests path/adapter.js
(其中path/adapter.js
是具体Adapter文件所在的位置),就能看到测试结果。
promises-aplus-tests共有872条测试用例,测试只覆盖Promise/A+规范中要求的部分,所以只要求Promise必须包含then
方法,并不关心其他的catch
或者all
等方法。
如果你下载了toy-promise仓库,则直接执行
npm test
即可。
如果对本文有什么意见和建议,欢迎讨论和指正!