写在前面
对于前端新手而言,Promise是一件比较困扰学习的事情,需要理解的细节比较多。
对于前端面试而言,Promise是面试官最常问的问题,特别是手撕源码。
众所周知,JavaScript
语言执行环境是“单线程”。
单线程,就是指一次只能完成一件任务,如果有多个任务就必须排队等候,前面一个任务完成,再执行后面一个任务。这种“单线程”模式执行效率较低,任务耗时长。
为了解决这个问题,就有了异步模式,也叫异步编程。
一、异步编程
所谓”异步”,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,当第一段有了执行结果之后,再回过头执行第二段。
JavaScript采用异步编程原因有两点:
- JavaScript是单线程。
- 为了提高CPU的利用率。
在提高CPU的利用率的同时也提高了开发难度,尤其是在代码的可读性上。
那么异步存在的场景有:
- fs 文件操作
require("fs").readFile("./index.html",(err,data)=>{})
复制代码
- 数据库操作
- AJAX
$.get("/user",(data)=>{})
复制代码
- 定时器
setTimeout(()=>{},2000)
复制代码
二、Promise是什么
Promise理解
(1) 抽象表达
Promise
是一门新的技术(es6
规范)Promise
是js
中进行异步编程的新解决方案
(2) 具体表达
- 从语法上说:
Promise
是一个构造函数 - 从功能上说:
Promise
对象是用来封装一个异步操作并可以获取其成功/失败的结果值
为什么要使用Promise
(1) 指定回调函数的方式更加灵活
promise
:启动异步任务=>返回promise
对象=>给promise
对象绑定回调函数
(2) 支持链式调用方式,可以解决回调地狱问题
- 什么是回调地狱?
回调地狱就是回调函数嵌套使用,外部回调函数异步执行的结果是嵌套的回调执行的条件
- 回调地狱的缺点
- 不便于阅读
- 不便于异常处理
- 解决方法
Promise的状态
- Promise必须拥有三种状态:pending、rejected、resolved
- 如果Promise的状态是pending时,它可以变成成功fulfilled或失败rejected
- 如果promise是成功状态,则它不能转换为任何状态,而且需要一个成功的值,并且这个值不能改变
- 如果promise是失败状态,则它不能转换成任何状态,而且需要一个失败的原因,并且这个值不能改变
Promise的状态改变
pending
未决定的,指的是实例状态内置的属性
(1)pending
变为resolved/fullfilled
(2)pending
变为rejected
说明:Promise的状态改变只有两种,且一个Promise
对象只能改变一次,无论失败还是成功都会得到一个结果输出,成功的结果一般是value
,失败的结果一般是reason
。
无论状态是成功还是失败,返回的都是promise
。
Promise的值
实例对象中的另一个属性 [PromiseResult
]保存着异步任务 [成功/失败] 的结果resolve/reject
。
Promise的api
手写Promide中的api:
(1)promise
构造函数 Promise(executor){}
executor
:执行器(resolve,reject)=>{}
resolve
:内部定义成功时我们需要调用的函数value=>{}
reject
:内部定义失败时我们调用的函数reason=>{}
说明:executor会在Promise内部立即同步调用,异步操作在执行器中执行
(2)Promise.prototype.then
方法:(onResolved,rejected)=>{}
onResolved
函数:成功的回调函数value=>{}
rejected
函数:失败的回调函数reason=>{}
说明:指定用于得到成功value的成功回调和用于得到失败reason的失败回调,返回一个新的promise对象
(3)Promise.prototype.catch
方法:(onRejected)=>{}
前三条是本文章中将要实现的手写代码,当然Promise还有其它的api接口。
(1)Promise.prototype.finally()
方法
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码
(2)Promise.all()方法
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
复制代码
p的状态由p1、p2、p3决定,分成两种情况。
-
只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
-
只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
(3)Promise.race()方法
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
复制代码
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
(4)Promise.allSettled()方法
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
复制代码
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。
(5)Promise.any()方法
ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
(6)Promise.reject(reason)方法
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
复制代码
(7)Promise.resolve()方法
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
复制代码
改变promsie状态和指定回调函数谁先谁后?
(1)都有可能,正常情况下是先指定回调函数再改变状态,但也可以先改变状态再指定回调函数。
(2)如何改变状态再指定回调?
- 在执行器中直接调用
resolve/reject
- 延迟更长时间才进行调用
then
(3)什么时候才能得到数据?
- 如果先指定的回调,那当状态发生改变,回调函数就会调用,得到数据
- 如果先改变的状态,那当指定回调时,回调函数就会进行调用,得到数据
示例:
let p = new Promise((resolve,reject)=>{
resolve("成功了");
reject("失败了");
});
p.then((value)=>{
console.log(value);
},(reason)=>{
console.log(reason);
})
复制代码
Promise规范
- “Promise”是一个具有then方法的对象或函数,其行为符合此规范。也就是说Promise是一个对象或函数
- “thenable”是一个具有then方法的对象或函数,也就是这个对象必须拥有then方法
- “value”是任何合法的js值(包括undefined或promise)
- promise中的异常需要使用throw语句进行抛出
- promise失败的时候需要给出失败的原因
then方法说明
- 一个promise必须要有一个then方法,而且可以访问promise最终的结果,成功或者失败的值
- then方法需要接收两个参数,onfulfilled和onrejected这两个参数是可选参数
- promise无论then方法是否执行完毕,只要promise状态变了,then中绑定的函数就会执行。
链式调用
Promise最大的优点就是可以进行链式调用,如果一个then方法返回一个普通值,这个值就会传递给下一次的then中,作为成功的结果。
如果返回的是一个promise,则会把promise的执行结果传递下去取决于这个promise的成功或失败。
如果返回的是一个报错,就会执行到下一个then的失败函数中。
三、手写Promise代码
面试经常考的手写Promise代码,可以仔细理解一下。
// 手写Promise
// 首先定义一个构造函数,在创建Promise对象的时候会传递一个函数executor,
// 这个函数会立即被调用,所以我们在Promise内部立即执行这个函数。
function Promise(executor){
// 用于保存promise的状态
this.status = "pending";
this.value;//初始值
this.reason;//初始原因
this.onResolvedCallbacks = [];//存放所有成功的回调函数
this.onRejectedCallbacks = [];//存放所有失败的回调函数
//定义resolve函数
const resolve = (value)=>{
if(this.status === "pending"){
this.status = "resolved";
this.value = value;
this.onResolvedCallbacks.forEach(function(fn){
fn()
})
}
}
//定义reject函数
const reject = (reason)=>{
if(this.status === "pending"){
this.status = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach(function(fn){
fn()
})
}
}
executor(resolve,reject);
}
Promise.prototype.then = function(onFulfilled,onRejected){
/*
每次then都会返回一个新的promise
我们需要拿到当前then方法执行成功或失败的结果,
前一个then方法的返回值会传递给下一个then方法,
所以这里我们要关心onFulfilled(self.value)
和 onRejected(self.reason)的返回值,我们这里定义一个x来接收一下。
如果失败抛错需要执行reject方法,这里使用try...catch捕获一下错误。
也就是判断then函数的执行结果和返回的promise的关系。
*/
return new Promise((resolve,reject)=>{
//当Promise状态为resolved时
if(this.status === "resolved"){
try{
resolve(onFulfilled(this.value))
}catch(error){
reject(error)
}
}
//当Promise状态为rejected时
if(this.status === "rejected"){
try {
resolve(onRejected(this.reason))
} catch (error) {
reject(error)
}
}
//当Promise状态为pendding
if(this.status === "pending"){
this.onResolvedCallbacks.push(function(){
try{
resolve(onFulfilled(this.value))
}catch(error){
reject(error)
}
});
this.onRejectedCallbacks.push(function(){
try {
resolve(onRejected(this.reason))
} catch (error) {
reject(error)
}
});
}
})
}
复制代码
升级版Promise:
class Promise{
/*首先定义一个构造函数,在创建Promise对象的时候会传递一个函数executor,
这个函数会立即被调用,所以我们在Promise内部立即执行这个函数。*/
constructor(executor){
this.executor = executor(this.resolve,this.reject);
this.onResolvedCallbacks = [];//存放所有成功的回调函数
this.onRejectedCallbakcs = [];//存放所有失败的回调函数
}
// 用于存储相应的状态
status = "pending";
// 初始值
value;
// 初始原因
reason;
// executor在执行的时候会传入两个方法,一个是resolve,
// 一个reject,所以我们要创建这两个函数,而且需要把这两个函数传递给executor。
// 当我们成功或者失败的时候,执行onFulfilled和onRejected的函数,
// 也就是在resolve函数中和reject函数中分别循环执行对应的数组中的函数。
// 定义成功事件
resolve(value){
if(status === "pending"){
status = "resolved";
value = value;
this.onResolvedCallbacks.forEach(fn=>{fn()})
}
}
// 定义失败事件
reject(){
if(this.status === "pending"){
this.status = "rejected";
this.reason = reason;
this.onRejectedCallbakcs.forEach(fn=>{fn()});
}
}
// 这个时候当我们异步执行resolve方法时候,then中绑定的函数就会执行,并且绑定多个then的时候,多个方法都会执行。
// Promise的对象存在一个then方法,这个then方法里面会有两个参数,一个是成功的回调onFulfilled,
// 另一个是失败的回调onRejected,只要我们调用了resolve就会执行onFulfilled,调用了reject就会执行onRejected。
// 为了保证this不错乱,我们定义一个self存储this。当我们调用了resolve或reject的时候,需要让状态发生改变.
// 需要注意的是Promise的状态只可改变一次,所以我们要判断,只有当状态未发生改变时,才去改变状态。
then(onFulfilled,onRejected){
// 判断当前状态进行回调
if(this.status === "resolved"){
onFulfilled(self.value)
};
if(this.status === "rejected"){
onRejected(self.reason)
}
// 当状态还处于pending状态时
// 因为onFulfilled和onRejected在执行的时候需要传入对应的value值,所我们这里用一个函数包裹起来,将对应的值也传入进去。
if(this.status === "pending"){
this.onResolvedCallbacks.push(()=>{onFulfilled(this.value)});
this.onResolvedCallbacks.push(()=>{onRejected(this.reason)});
}
}
}
复制代码
使用自己手写的Promise源码:
let p = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("成功了")
},1000)
});
p.then(function(value){
return 123;
}).then(value=>{
console.log("收到了成功的消息:",value);
}).catch(error=>{
console.log(error);
});
p.then(value=>{
console.log(value);
})
复制代码
四、Async/Await
async用来表示函数是异步的,定义的async函数返回值是一个promise对象,可以使用then方法添加回调函数。
await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。 函数中只要使用await,则当前函数必须使用async修饰。
所以回调函数的终结者就是async/await。
async命令
- async函数返回的是一个promise对象。
- async函数内部return语句返回的值,会成为then方法回调的参数。
- async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。
- 抛出的错误对象会被catch方法回调函数接收到。
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。
也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function fun(){
// return "hello wenbo";
throw new Error("ERROR");
}
fun().then(v => console.log(v),reason=>console.log(reason));//Error: ERROR```
复制代码
await命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function fun(){
return await "zhaoshun";
// 等价于 return "zhaoshun";
}
fun().then(value=>console.log(value));//zhaoshun
复制代码
另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。
class Sleep{
constructor(timeout){
this.timeout = timeout;
}
then(resolve,reject){
const startTime = Date.now();
setTimeout(()=>resolve(Date.now() - startTime),this.timeout);
}
}
(
async ()=>{
const sleepTime = await new Sleep(1000);
console.log(sleepTime);//1012
}
)()
// js里面没有休眠的语法,但是借助await命令可以让程序停顿的时间
const sleepFun = (interval) => {
return new Promise(resolve=>{
setTimeout(resolve,interval);
})
}
// 用法
const asyncFun = async ()=>{
for(let i = 1; i <= 5; i++){
console.log(i);
await sleepFun(1000);
}
}
asyncFun();
复制代码
从上面可以看到,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。
await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
注意:上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。
- 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
- 使用try/catch可以很好处理前面await中断,而后面不执行的情况。
示例:
const fun = async ()=>{
try {
await Promise.reject("ERROR");
} catch (error) {
}
return await Promise.resolve("success");
}
fun().then(
value=>console.log(value),reason=>console.log(reason,"error")//
).catch(
error=>console.log(error)//ERROR
);
复制代码
另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
const fun = async ()=>{
await Promise.reject("error").catch(e=>console.log(e));
return await Promise.resolve("success");
}
fun().then(v=>console.log(v));//success
复制代码
错误处理
第一点:如果await后面的异步操作出错,那么等同于async函数后面的promise对象被reject。
const fun = async()=>{
await new Promise((resolve,reject)=>{
throw new Error("error")
})
}
fun().then(v=>console.log(v)).catch(e=>console.log(e));
复制代码
第二点:多个await命令后面的异步操作,如果不存在继发关系,最好让他们同时进行触发。
const [fun1,fun2] = await Promise.all([getFun(),getFoo()]);
const fooPromise = getFoo();
const funPromise = getFun();
const fun1 = await fooPromise();
const fun2 = await funPromise();
复制代码
第三点:await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
复制代码
第四点:async
函数可以保留运行堆栈。
小结
在这篇文章中我们总结了异步编程和回调函数的解决方案Promise
,以及回调终结者aysnc/await
。
- Promise有三个状态:
pending
、rejected
、resolved
。 - Promise的状态改变,只能改变一次,只有两种改变:
pending
变为resolved/fullfilled
、pending
变为rejected
。 - Promise最大优点就是可以进行链式调用。
async
用来表示函数是异步的,定义的async函数返回值是一个promise对象。- 函数中只要使用
await
,则当前函数必须使用async修饰。 - 回调函数的终结者就是
async/await
。
参考文章
《BAT前端经典面试问题:史上最最最详细的手写Promise教程》
《JavaScript高级程序设计(第四版)》
《你不知道的JavaScript(中卷)》
写在最后
我是前端小菜鸡,感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。