Promise对象
【Promise对象】
同步异步的介绍
Promise 是异步操作的一种解决方案。
异步的概念
异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。
在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。
以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务。
什么时候用异步编程
在前端编程中(甚至后端有时也是这样),我们在处理一些简短、快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求。
现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。
为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情(或者是一些需要等待某个时机在背后自动执行的任务,比如:事件监听),比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
JavaScript 是单线程语言,为了解决多线程问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
回调函数(callback function)
在 JavaScript 中,回调函数具体的定义为:函数A 作为参数(函数引用)传递到另一个 函数B 中,并且这个 函数B 执行函数A。我们就说 函数A 叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
回调函数就是一个作为参数的函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
注意:回调和异步不是同一个东西,许多人误认为 js 中每个回调函数都是异步处理的,实际上并不是,可以同步回调,也可以异步回调。只不过说:回调可以是同步也可以是异步,异步必须放在回调里执行,也就是对于一个异步任务只有回调函数里的才是异步的部分。
回调同步的例子:
1
2
3
4
5
6
7 const test = function (func) {
func();
}
test(() => {
console.log('func');
})回调异步的例子:
1
2
3
4 setTimeout(()=>{
console.log('one');
}, 3000);
console.log('two');实例
setInterval()
和setTimeout()
是两个异步语句。异步(asynchronous):不会阻塞 CPU 继续执行其他语句,当异步完成时(包含回调函数的主函数的正常语句完成时),会执行 “回调函数”(callback)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<p>回调函数等待 3 秒后执行。</p>
<p id="demo"></p>
<p>异步方式,不影响后续执行。</p>
<script>
function print() {
document.getElementById("demo").innerHTML = "RUNOOB!";
}
setTimeout(print, 3000);
</script>
</body>
</html>
这段程序中的 setTimeout 就是一个消耗时间较长(3 秒)的过程,它的第一个参数是个回调函数,第二个参数是毫秒数,这个函数执行之后会产生一个子线程,子线程会等待 3 秒,然后执行回调函数 “print”,在命令行输出 “RUNOOB!”。
当然,JavaScript 语法十分友好,我们不必单独定义一个函数 print ,我们常常将上面的程序写成:
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<p>回调函数等待 3 秒后执行。</p>
<p id="demo"></p>
<p>异步方式,不影响后续执行。</p>
<script>
setTimeout(function () {
document.getElementById("demo").innerHTML = "RUNOOB!";
}, 3000);
/* ES6 箭头函数写法
setTimeout(() => {
document.getElementById("demo").innerHTML = "RUNOOB!";
}, 3000);
*/
</script>
</body>
</html>**注意:**既然 setTimeout 会在子线程中等待 3 秒,在 setTimeout 函数执行之后主线程并没有停止,所以:
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<p>回调函数等待 3 秒后执行。</p>
<p id="demo1"></p>
<p id="demo2"></p>
<script>
setTimeout(function () {
document.getElementById("demo1").innerHTML = "RUNOOB-1!";
}, 3000);
document.getElementById("demo2").innerHTML = "RUNOOB-2!";
</script>
</body>
</html>这段程序的执行结果是:
(之前常用的异步操作解决方案是:回调函数)
1 | document.addEventListener( |
什么时候使用 Promise 呢?
Promise 一般用来解决层层嵌套的回调函数(回调地狱 callback hell)的问题。
例如下面展示两个回调地狱的例子:
例子1:分别间隔一秒打印省市县
1 |
|
例子2:当我们点击窗口后,盒子依次 “右——>下——>左” 移动
1 |
|
style=“zoom:25%;” />
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 有三个状态:pending(等待)、fulfilled 或 resolved(成功)、rejected(失败)。
并且 Promise 必须接收一个回调函数,这个回调函数有两个参数,这两个参数也是两个函数,(resolve, reject) => {}
。
-
实例化 Promise 后,默认是等待状态。
-
当执行
resolve()
函数时,Promise 从等待状态——>成功状态。 -
当执行
reject()
函数时,Promise 从等待状态——>失败状态。
注意:当 Promise 的状态一但从等待转变为某一个状态后,后续的转变就自动忽略了,比如:先调用 resolve() 再调用 reject(),那么 Promise 的最终结果是成功状态。
注意:这里的 resolve reject 只是一个形参,可以取任意名字,但是我们约定直接使用 resolve reject。
注意,为了行文方便,本章后面的resolved
统一只指fulfilled
状态,不包含rejected
状态。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise 的基本用法
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
下面代码创造了一个Promise
实例。
1 | const promise = new Promise(function(resolve, reject) { |
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
resolve()
和 reject()
函数是可以接收参数的。
resolve()
接收的参数会传递给 then 方法的第一个回调函数reject()
接收的参数会传递给 then 方法的第二个回调函数
注意:通常我们不仅仅会传递一个基本数据类型的值,我们还常常传递对象,比如再 reject 中传递一个错误对象:
reject(new Error("出错了!"));
1 | promise.then(function(value) { |
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise
对象传出的值作为参数。
下面是一个Promise
对象的简单例子。
1 | function timeout(ms) { |
上面代码中,timeout
方法返回一个Promise
实例,表示一段时间以后才会发生的结果。过了指定的时间(ms
参数)以后,Promise
实例的状态变为resolved
,就会触发then
方法绑定的回调函数。
Promise 新建后就会立即执行。
1 | let promise = new Promise(function(resolve, reject) { |
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise
。然后,then
方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved
最后输出。
下面是异步加载图片的例子。
1 | function loadImageAsync(url) { |
上面代码中,使用Promise
包装了一个图片加载的异步操作。如果加载成功,就调用resolve
方法,否则就调用reject
方法。
如果调用resolve
函数和reject
函数时带有参数,那么它们的参数会被传递给回调函数。reject
函数的参数通常是Error
对象的实例,表示抛出的错误;resolve
函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
1 | const p1 = new Promise(function (resolve, reject) { |
上面代码中,p1
和p2
都是 Promise 的实例,但是p2
的resolve
方法将p1
作为参数,即一个异步操作的结果是返回另一个异步操作。
注意,这时p1
的状态就会传递给p2
,也就是说,p1
的状态决定了p2
的状态。如果p1
的状态是pending
,那么p2
的回调函数就会等待p1
的状态改变;如果p1
的状态已经是resolved
或者rejected
,那么p2
的回调函数将会立刻执行。
1 | const p1 = new Promise(function (resolve, reject) { |
上面代码中,p1
是一个 Promise,3 秒之后变为rejected
。p2
的状态在 1 秒之后改变,resolve
方法返回的是p1
。由于p2
返回的是另一个 Promise,导致p2
自己的状态无效了,由p1
的状态决定p2
的状态。所以,后面的then
语句都变成针对后者(p1
)。又过了 2 秒,p1
变为rejected
,导致触发catch
方法指定的回调函数。
注意,调用resolve
或reject
并不会终结 Promise 的参数函数的执行。
1 | new Promise((resolve, reject) => { |
上面代码中,调用resolve(1)
以后,后面的console.log(2)
还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外。
1 | new Promise((resolve, reject) => { |
Promise.prototype.then()
-
then 方法的两个回调函数什么时候执行
- pending——>resolved时,执行 then 的第一个回调函数
- pending——>rejected 时,执行 then 的第二个回调函数
-
then 方法执行后的返回值
- then 方法执行后默认自动返回一个新的 Promise 对象
-
then 方法返回的 Promise 对象的状态改变
-
then 方法其实默认返回的是 undefined,即:
return undefined
,但是 ES6 的机制规定:当 then 返回 undefined 时,那么会将这个 undefined 包装成一个 Promise,并且这个 Promise 默认调用了resilve()
方法(成功态),并且把 undefined 作为了 resilve() 的参数,相当于:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32const p = new Promise((resolve, reject) => {
resolve();
});
p.then(() => {
// 默认会执行这一条
// return undefined;
}, () => {
});
// 实际上,return 会包装为一个 Promise 对象,同时默认执行 resolve(),并把 return 的值作为 resolve() 的参数
/*
return new Promise(resolve => {
resolve(undefined);
});
*/
// -----------------------------
// 如果我们在这个返回的 Promise 上继续调用 then 方法,并接收参数的话,可以发现 then 中成功接收到了被 Promise 包装后的参数
const p2 = new Promise((resolve, reject) => {
resolve();
});
p2.then(() => {
// 默认会执行这一条
// return undefined;
}).then(data => {
console.log(data); // 打印 undefined
// 手动 return 一个值
return 24;
// 相当于:return new Promise(resolve => {resolve(24);});
}).then((data) => {
console.log(data); // 打印 24
}); -
如果我们要让 then 返回一个失败状态的 Promise,那么我们可以手动 return 一个 Promise 并执行 reject() 方法。
1
2
3
4
5
6
7
8
9
10
11const p3 = new Promise((resolve, reject) => {
resolve();
});
p3.then(() => {
// 手动返回一个调用了 reject 的 Promise
return new Promise((resolve, reject) => {
reject("失败");
})
}).then(() => {}, errData => {
console.log(errData); // 失败
});
-
总结:Promise 是一个构造函数,需要 new 才能使用。在 new Promise() 的时候需要传递一个匿名回调函数作为 Promise() 唯一的参数,这个回调函数有两个参数 resolve reject,这两个参数也是函数,当回调函数执行第一个 resolve 函数后 Promise 便变为了成功状态,反之回调函数执行了 reject 后 Promise 便变为了失败状态,且每个 Promise 只能要么执行 resolve,要么执行 reject,不能同时执行!当 Promise 被 new 之后就会有一个 then 方法,该方法默认接收两个匿名回调函数作为参数,其中第一个回调函数是在 Promise 为成功状态时自动调用的,反之第二个回调函数是在 Promise 为失败状态时自动调用的,并且这两个回调函数是可以接收参数的,参数就来自于 resolve 或 reject 调用时传递的实参!在 then 方法执行后会默认返回 undefined(在没有指定返回值的情况下),ES6 会将其包装为一个新的成功态的 Promise,该 Promise 会自动执行 resolve 函数,该函数的参数来自于 then 方法的返回值(如果没有返回值那么默认就返回 undefined)。如果需要返回一个失败态的 Promise,那么需要在 then 中手动指定返回值:
1
2
3 return new Promise((resolve, reject) => {
reject(参数);
}
案例:分别间隔一秒打印省市县。
1 | <!DOCTYPE html> |
Promise.prototype.catch()
由之前的例子可以看出,我们在使用 Promise 的时候,大部分情况下,我们只用 resolve() 方法(成功态),所以在 Promise 回调函数中我们常常省略 reject 函数参数,在 then 中我们常常省略第二个回调函数。
但是我们还是需要处理异步中的异常,所以 ES6 中提供了我们一个 catch()
方法专门用来处理 Promise 的异常部分(失败态)。
-
catch 专门用来处理 rejected 状态
-
catch 本质上是 then 的特例
1 | new Promise((resolve, reject) => { |
在 Promise 中,一但出现了错误状态,那么这个错误是不会消失的,会一直向下传递,直到遇到可以处理错误的函数。
由于 catch 是 then 的特例,所以 catch 依旧返回的是一个 Promise 对象,我们可以在 catch 后继续调用 then。
1 | new Promise((resolve, reject) => { |
一般总是建议,Promise 对象后面要跟一个或多个 catch 方法,这样可以处理 Promise 内部发生的错误!
Promise.prototype.finally()
当 Promise 状态发生变化时,不论如何变化都会执行,不变化不执行。
-
finally() 不能接收参数。
-
finally 也是 then 的特例。
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
1 | promise |
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
1 | server.listen(port) |
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
上面代码中,如果不使用finally
方法,同样的语句需要为成功和失败两种情况各写一次。有了finally
方法,则只需要写一次。
1 | new Promise(resolve => { |
finally
:主要是用来处理一些必做操作,比如在操作数据库之后(无论成功与否)都要关闭数据库连接。
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.all([p1, p2, p3]); |
上面代码中,Promise.all()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
下面是一个具体的例子。
1 | // 生成一个Promise对象的数组 |
上面代码中,promises
是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled
,或者其中有一个变为rejected
,才会调用Promise.all
方法后面的回调函数。
1 | /* |
Promise.resolve()和Promise.reject()
以上两者都是 Promise 构造函数的方法。
下面我们以 Promise.resolve() 举例,Promise.reject() 同理。
1 | // Promise.resolve() 可以理解为普通成功状态的一种简写形式 |
Promise.resolve() 与 Promise.reject() 的参数问题:
1、一般参数
1 | Promise.resolve('foo').then(data => { |
2、特殊参数:Promise 作为参数
1 | const p1 = new Promise(resolve => { |
3、特殊参数:具有 then 方法的对象(了解即可)
1 | const thenable = { |
与 Promise.resolve() 不同,Promise.reject() 无论接收什么类型的参数,都会原封不动的向后传递!
Promise.race()
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.race([p1, p2, p3]); |
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
Promise.race()
方法的参数与Promise.all()
方法一样,如果不是 Promise 实例,就会先调用Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject
,否则变为resolve
。
1 | const p = Promise.race([ |
上面代码中,如果 5 秒之内fetch
方法无法返回结果,变量p
的状态就会变为rejected
,从而触发catch
方法指定的回调函数。
Promise.allSettled()
有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。但是,现有的 Promise 方法很难实现这个要求。
Promise.all()
方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。
1 | const urls = [url_1, url_2, url_3]; |
上面示例中,Promise.all()
可以确定所有请求都成功了,但是只要有一个请求失败,它就会报错,而不管另外的请求是否结束。
为了解决这个问题,ES2020引入了Promise.allSettled()
方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。
Promise.allSettled()
方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled
还是rejected
),返回的 Promise 对象才会发生状态变更。
1 | const promises = [ |
上面示例中,数组promises
包含了三个请求,只有等到这三个请求都结束了(不管请求成功还是失败),removeLoadingIndicator()
才会执行。
该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
1 | const resolved = Promise.resolve(42); |
上面代码中,Promise.allSettled()
的返回值allSettledPromise
,状态只可能变成fulfilled
。它的回调函数接收到的参数是数组results
。该数组的每个成员都是一个对象,对应传入Promise.allSettled()
的数组里面的两个 Promise 对象。
results
的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。
1 | // 异步操作成功时 |
成员对象的status
属性的值只可能是字符串fulfilled
或字符串rejected
,用来区分异步操作是成功还是失败。如果是成功(fulfilled
),对象会有value
属性,如果是失败(rejected
),会有reason
属性,对应两种状态时前面异步操作的返回值。
下面是返回值的用法例子。
1 | const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ]; |
Promise的应用
【异步加载图片】
异步加载:也称为图片的预加载。利用 js 代码提前加载图片,用户需要时可以直接从本地缓存获取,但是会增加服务器前端的压力。这样做可以提高用户的体验,因为同步加载大图片的时候,图片会一层一层的显示处理,但是经过预加载后,直接显示出整张图片。
1 |
|