ES6的知识点总结(三)
文章目录
【Promise对象】
1. 同步异步的介绍
Promise 是异步操作的一种解决方案。
异步的概念
异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。
在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。
以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务。
什么时候用异步编程
在前端编程中(甚至后端有时也是这样),我们在处理一些简短、快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求。
现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。
为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情(或者是一些需要等待某个时机在背后自动执行的任务,比如:事件监听),比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
JavaScript 是单线程语言,为了解决多线程问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
回调函数(callback function)
在 JavaScript 中,回调函数具体的定义为:函数A 作为参数(函数引用)传递到另一个 函数B 中,并且这个 函数B 执行函数A。我们就说 函数A 叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
回调函数就是一个作为参数的函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
注意:回调和异步不是同一个东西,许多人误认为 js 中每个回调函数都是异步处理的,实际上并不是,可以同步回调,也可以异步回调。只不过说:回调可以是同步也可以是异步,异步必须放在回调里执行,也就是对于一个异步任务只有回调函数里的才是异步的部分。
回调同步的例子:
const test = function (func) {
func();
}
test(() => {
console.log('func');
})
回调异步的例子:
setTimeout(()=>{
console.log('one');
}, 3000);
console.log('two');
实例
setInterval()
和 setTimeout()
是两个异步语句。
异步(asynchronous):不会阻塞 CPU 继续执行其他语句,当异步完成时(包含回调函数的主函数的正常语句完成时),会执行 “回调函数”(callback)
<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>
什么时候使用 Promise 呢?
Promise 一般用来解决层层嵌套的回调函数(回调地狱 callback hell)的问题。
例如下面展示两个回调地狱的例子:
分别间隔一秒打印省市县
// 通过回调函数的方式,实现异步
setTimeout(() => {
console.log("云南省");
let str01 = "云南省";
setTimeout(() => {
console.log(str01 + "玉溪市");
let str02 = "云南省玉溪市";
setTimeout(() => {
console.log(str02 + "峨山县");
}, 1000, str02);
}, 1000, str01);
}, 1000);
2. 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。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展
3. Promise的基本用法
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
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("出错了!"));
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise对象的状态变为rejected
时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
下面是异步加载图片的例子。
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。
如果调用resolve
函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject
函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
上面代码中,p1和p2都是 Promise 的实例,但是p2的resolve
方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending
,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved
或者rejected
,那么p2的回调函数将会立刻执行。
4. Promise.prototype.then()
1.then
方法的两个回调函数什么时候执行
pending
——>resolved
时,执行then
的第一个回调函数pending
——>rejected
时,执行then
的第二个回调函数
2.then
方法执行后的返回值
- then 方法执行后默认自动返回一个新的 Promise 对象
3.then
方法返回的 Promise 对象的状态改变
-
then 方法其实默认返回的是
undefined
,即:return undefined
,但是 ES6 的机制规定:当 then 返回undefined
时,那么会将这个undefined
包装成一个Promise
,并且这个Promise
默认调用了resilve()
方法(成功态),并且把undefined
作为了resilve()
的参数 -
如果我们要让 then 返回一个失败状态的 Promise,那么我们可以手动 return 一个 Promise 并执行 reject() 方法。
const p3 = new Promise((resolve, reject) => {
resolve();
});
p3.then(() => {
// 手动返回一个调用了 reject 的 Promise
return new Promise((resolve, reject) => {
reject("失败");
})
}).then(() => {}, errData => {
console.log(errData); // 失败
});
案例:分别间隔一秒打印省市县。
<body>
<script>
// 通过 Promise 的方式,解决回调地狱
new Promise((resolve) => {
setTimeout(() => {
console.log("云南省");
resolve("云南省");
}, 1000);
}).then(res => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(res + "玉溪市");
resolve(res + "玉溪市");
}, 1000);
});
}).then(res => {
setTimeout(() => {
console.log(res + "峨山县");
}, 1000);
});
console.log("通过 Promise 的方式,实现异步");
</script>
5. Promise.prototype.catch()
由之前的例子可以看出,我们在使用 Promise 的时候,大部分情况下,我们只用 resolve()
方法(成功态),所以在 Promise
回调函数中我们常常省略 reject
函数参数,在 then
中我们常常省略第二个回调函数。
但是我们还是需要处理异步中的异常,所以 ES6 中提供了我们一个 catch()
方法专门用来处理 Promise 的异常部分(失败态)。
-
catch 专门用来处理 rejected 状态
-
catch 本质上是 then 的特例
new Promise((resolve, reject) => {
reject("失败");
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err); // 失败
});
// -------------------------------------
// 上面的代码本质上等同于
new Promise((resolve, reject) => {
reject("失败");
}).then(res => {
console.log(res);
}).then(null, err => {
console.log(err); // 失败
});
在 Promise 中,一但出现了错误状态,那么这个错误是不会消失的,会一直向下传递,直到遇到可以处理错误的函数。
由于 catch
是 then 的特例,所以 catch 依旧返回的是一个 Promise 对象,我们可以在 catch 后继续调用 then。
new Promise((resolve, reject) => {
reject("失败");
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err); // 失败
return "测试";
}).then(res => {
console.log(res); // 测试
});
一般总是建议,Promise 对象后面要跟一个或多个 catch 方法,这样可以处理 Promise 内部发生的错误!
6. Promise.prototype.finally()
当 Promise 状态发生变化时,不论如何变化都会执行,不变化不执行。
-
finally()
不能接收参数。 -
finally
也是then
的特例。
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
finally
:主要是用来处理一些必做操作,比如在操作数据库之后(无论成功与否)都要关闭数据库连接。
7. Promise.race()
Promise.race()
方法是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
如果不是 Promise 实例,就会先调用Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject
,否则变为resolve
。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
8. Promise.allSettled()
有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。但是,现有的 Promise 方法很难实现这个要求。
Promise.all()
方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。
const urls = [url_1, url_2, url_3];
const requests = urls.map(x => fetch(x));
try {
await Promise.all(requests);
console.log('所有请求都成功。');
} catch {
console.log('至少一个请求失败,其他请求可能还没结束。');
}
上面示例中,Promise.all()
可以确定所有请求都成功了,但是只要有一个请求失败,它就会报错,而不管另外的请求是否结束。
为了解决这个问题,ES2020引入了Promise.allSettled()
方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。
Promise.allSettled()
方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
上面示例中,数组promises
包含了三个请求,只有等到这三个请求都结束了(不管请求成功还是失败),removeLoadingIndicator()
才会执行。
该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
上面代码中,Promise.allSettled()
的返回值allSettledPromise
,状态只可能变成fulfilled。它的回调函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()
的数组里面的两个 Promise 对象。
results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。
// 异步操作成功时
{status: 'fulfilled', value: value}
// 异步操作失败时
{status: 'rejected', reason: reason}
成员对象的status属性的值只可能是字符串fulfilled或字符串rejected,用来区分异步操作是成功还是失败。如果是成功(fulfilled
),对象会有value
属性,如果是失败(rejected
),会有reason
属性,对应两种状态时前面异步操作的返回值。
下面是返回值的用法例子。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);