主要学习和摘录《JavaScript重难点实例精讲》中的内容
1、Promise产生的原因
多个异步请求,想要顺序执行,就产生了回调地狱的写法:
setTimeout(() => {
console.log("1");
setTimeout(() => {
console.log("2");
setTimeout(() => {
console.log("3");
}, 1000);
}, 1000);
}, 1000);
console.log("A");
//执行结果是:A、1、2、3
“回调地狱”存在以下几个问题:
- 代码臃肿,可读性差。
- 代码耦合度高,可维护性差,难以复用。
- 回调函数都是匿名函数,不方便调试。
Promise为异步编程提供了一种更合理、更强大的解决方案。
2、Promise的生命周期
Promise对象有三种状态:
- pending,进行中
- fulfilled,成功
- rejected,失败
Promise对象创建后,状态为pending,执行后,要么成功,要么失败,状态只改变一次。
3、基本用法
3.1 创建Promise对象
语法:
const promise = new Promise((resolve, reject) => {
// 异步请求处理
if(/ 异步请求标识 /) {
resolve();
} else {
reject();
}
});
示例:
const p1 = new Promise((resolve) => {
setTimeout(() => {
console.log("1");
resolve(true);
}, 1000);
});
console.log("A");
//输出:A、1
注意:
- Promise对象一旦创建,就会立马执行。
- 执行时等待执行resolve或reject来确定Promise的最终状态是成功还是失败。
- resolve()函数和reject()函数可以传递参数,作为后续.then()函数或者.catch()函数执行时的数据源。
3.2 执行顺序的判断
const p1 = new Promise((resolve) => {
console.log("1");
resolve("2");
});
p1.then((res) => {
console.log(res);
});
console.log("A");
执行结果是:1、A、2。
分析:Promise对象创建后立马输出1、然后执行then前会先把当前线程中的所有同步代码执行完,所以输出A,最后执行then输出2.
更多判断方法,参考事件循环-宏任务和微任务。
3.3 then函数
then函数是Promise状态改变后要执行的回调函数。
- 第一个参数是Promise状态变为成功后,要执行的回调函数(参数是resolve传递的参数)。
- 第二个参数是Promise状态变为失败后,要执行的回调函数(调用了reject或抛出了异常;参数是reject传递的参数)
- 可以链式调用,then的返回值会传给后面的then。
const p1 = new Promise((resolve) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return 2;
})
.then((res) => {
console.log(res);
return 3;
})
.then((res) => {
console.log(res);
});
console.log("A");
3.4 catch函数
一般不推荐在then的第二个回调函数中处理reject的情况,而是通过catch来捕捉问题。(一方面是then的第二个回调不能捕捉第一个回调里面的异常,见下文场景2中的例子;另一方面不然每次then都需要提供第二个回调来处理异常)
const p1 = new Promise((resolve) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return 2;
})
.then((res) => {
throw new Error("test err");
console.log(res);
return 3;
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
console.log("A");
输出:A、1、Error: test err。
执行第二个then的时候抛出了异常,后面就都不会执行了。
事实上只要在Promise执行过程中出现了异常,就会被自动抛出,并触发reject(err),而不用我们去使用try…catch,在catch()函数中手动调用reject()函数。
需要注意的是,如果一个Promise的状态已经变成fulfilled成功状态,再去抛出异常,是无法触发catch()函数的。这是因为Promise的状态一旦改变,就会永久保持该状态,不会再次改变。
3.5 Promise静态函数
3.5.1 Promise.all()
const p = Promise.all([p1, p2, p3])
用于将多个Promise实例包装成一个新的Promise实例,返回的新Promise实例p的状态由3个Promise实例p1、p2、p3共同决定,总共会出现以下两种情况。
- 只有p1、p2、p3全部的状态都变为fulfilled成功状态,p的状态才会变为fulfilled状态,此时p1、p2、p3的返回值组成一个数组,作为p的then()函数的回调函数的参数。
- 只要p1、p2、p3中有任意一个状态变为rejected失败状态,p的状态就变为rejected状态,此时第一个被reject的实例的返回值会作为p的catch()函数的回调函数的参数。
- 需要注意的是,作为参数的Promise实例p1、p2、p3,如果已经定义了catch()函数,那么当其中一个Promise状态变为rejected时,并不会触发Promise.all()函数的catch()函数。
- 如果想要Promise.all()函数能触发catch()函数,那么就不要在p1、p2实例中定义catch()函数。
3.5.2 Promise.race()
表示的是如果多个Promise实例中有任何一个实例的状态发生改变,那么这个新实例的状态就随之改变,而最先改变的那个Promise实例的返回值将作为新实例的回调函数的参数。
const p = Promise.race([p1, p2, p3])
使用Promise.race()函数可以实现这样一个场景:假如发送一个Ajax请求,在3秒后还没有收到请求成功的响应时,会自动处理成请求失败。
const p1 = ajaxGetPromise('/testUrl');
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
});
const p = Promise.race([p1, p2]);
p.then(console.log).catch(console.error);
3.5.3 Promise.resolve()
用于将传入的变量转换为Promise对象,它等价于在Promise函数体内调用resolve()函数。
Promise.resolve('hello');
// 等价于
new Promise(resolve => resolve('hello'));
3.5.4 Promise.reject()
函数在执行后Promise的状态会立即变为rejected,从而会立即进入catch()函数中做处理,等价于在Promise函数体内调用reject()函数。
const p = Promise.reject('出错了');
// 等价于
const p = new Promise((resolve, reject) => reject('出错了'));
4、用法实例
场景1:Promise代码与同步代码在一起执行
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
执行结果:1、2、4、3
- Promise在创建后会立即执行,所有同步代码按照书写的顺序从上往下执行,包括Promise外的同步代码,因此会先输出“1 2 4”。
- resolve()函数或者reject()函数会在同步代码执行完毕后再去执行。
- 当resolve()函数或者reject()函数执行后,进入then()函数或者catch()函数中执行,实例中调用了resolve()函数,会进行到then()函数中,因此会再输出“3”。
更多判断方法,参考事件循环-宏任务和微任务。
场景2:同一个Promise实例内,resolve()函数和reject()函数先后执行
const promise2 = new Promise((resolve, reject) => {
resolve("success1");
reject("error");
resolve("success2");
console.log('start');
});
promise2
.then((res) => {
console.log("then: ", res);
})
.catch((err) => {
console.log("catch: ", err);
});
执行结果:
start
then: success1
一个Promise的实例只能有一次状态的变更,当执行了resolve()函数后,后续其他的reject()函数和resolve()函数都不会执行,然后Promise进入then()函数中做处理。
场景3:同一个Promise实例自身重复执行
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once');
resolve('success');
}, 1000);
});
const start = Date.now();
promise3.then((res) => {
console.log(res, Date.now() - start);
});
promise3.then((res) => {
console.log(res, Date.now() - start);
});
执行结果:
once
success 1015
success 1016
同一个Promise的实例只能有一次状态变换的过程,在状态变换完成后,如果成功会触发所有的then()函数,如果失败会触发所有的catch()函数。
具体的秒数差取决于运行的环境,很可能会相差几毫秒。
场景4:在then()函数中返回一个异常
Promise.resolve()
.then(() => {
console.log(1);
return new Error("error!!!");
})
.then((res) => {
console.log(2);
console.log("then: ", res);
})
.catch((err) => {
console.log(3);
console.log("catch: ", err);
});
执行结果:
1
2
then: Error: error!!!
在then()函数中用return关键字返回了一个“Error”,依然会按照正常的流程走下去,进入第二个then()函数,并将Error实例作为参数传递,不会执行后续的catch()函数。
这个不同于使用throw抛出一个Error,如果是throw抛出一个Error则会被catch()函数捕获。
场景5:then()函数接收的参数不是一个函数
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log);//1
在Promise的then()函数或者catch()函数中,接收的是一个函数,函数的参数是resolve()函数或者reject()函数的返回值。
而如果传入的值是非函数,那么就会产生值穿透现象:传递的值会被直接忽略掉,继续执行链式调用后续的函数。
场景6:两种方法处理rejected状态的Promise
Promise.resolve()
.then(
function success(res) {
throw new Error("error");
},
function fail1(e) {
console.error("fail1: ", e);
}
)
.catch(function fail2(e) {
console.error("fail2: ", e);
});
执行结果:fail2: Error: error
虽然这两种方法都能处理Promise状态变为rejected时的回调,但是then()函数的第二个函数却不能捕获第一个函数中抛出的异常,而catch()函数却能捕获到第一个函数中抛出的异常。
这也是我们推荐使用catch()函数去处理Promise状态异常回调的原因。