一、背景
JavaScript是单线程语言,所以在执行耗时较大的任务时会导致页面阻塞。为了解决页面阻塞,异步执行被提了出来。但是异步带来一个执行顺序的问题,通过回调函数的嵌套,可以实现异步任务的依次执行,但是嵌套的回调函数代码耦合性强,阅读起来不直观,一旦嵌套过多,就会产生所谓的回调地狱,会带来以下问题:
1. 代码臃肿
2. 可读性差
3. 代码复用性差
4. 耦合高,维护性差
5. 只能在回调中处理异常
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
二、什么是Promise
1. 概念
Promise 是异步编程的一种解决方案,其实是一个构造函数,自己身上有all、reject、resolve这几个方法,原型上有then、catch等方法。所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
2. 特点
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
三、 Promise的使用
1. 基本用法
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
resolve和reject这两个回调可以在生成Promise实例后,调用then方法定义
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
Promise 新建后就会立即执行。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then()
then 方法就是把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
而 Promise 的优势就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。
可有两个参数,第一个是成功 resolve 调用的方法,第二个是失败 reject 调用的方法
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
/*
Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
*/
2. Promise 基本API
(1)实例方法
.then()
得到promise内部任务的执行结果。
.catch()
得到promise内部任务执行失败的结果。
.finally()
成功与否都会执行 在项目中 我们基于axios发送请求时(axios其实也是一个基于ajax用promise封装的http请求库),往往会开启loading,我们可以通过finally,不管请求成功与否,关闭loading,而不需要在成功和失败的时候写两次关闭loading代码。)
<script type="text/javascript">
/*
Promise常用API-实例方法
*/
// console.dir(Promise);
function foo() {
return new Promise(function(resolve, reject){
setTimeout(function(){
// resolve(123);
reject('error');
}, 100);
})
}
// foo()
// .then(function(data){
// console.log(data)
// })
// .catch(function(data){
// console.log(data)
// })
// .finally(function(){
// console.log('finished')
// });
// --------------------------
// 两种写法是等效的
foo()
.then(function(data){
# 得到异步任务正确的结果
console.log(data)
},function(data){
# 获取异常信息
console.log(data)
})
# 成功与否都会执行(不是正式标准)
.finally(function(){
console.log('finished')
});
</script>
.all()
Promise.all方法接受一个数组作参数,数组中的对象(p1、p2、p3)均为promise实例(如果不是一个promise,该项会被用Promise.resolve转换为一个promise)。它的状态由这三个promise实例决定
小案例
现在有这样的一个需求,执行按钮的触发条件是必须以上两个表单都验证成功,那么可以通过将每个表单的验证结果包装成promise对象,通过Promise.all 实现
function promise1() {
return new Promise(function (resolve, reject) {
this.$refs.form1.validate(v => {
if(v) resolve()
})
})
}
function promise2() {
return new Promise(function (resolve, reject) {
this.$refs.form2.validate(v => {
if(v) resolve()
})
})
}
Promise.all([promise1(), promise2()]).then(resolve => console.log('按钮触发事件')
).catch((e) => console.log(e))
.race()
Promise.race方法同样接受一个数组作参数。当p1, p2, p3中有一个实例的状态发生改变(变为fulfilled或rejected),p的状态就跟着改变。并把第一个改变状态的promise的返回值,传给p的回调函数。
<script type="text/javascript">
/*
Promise常用API-对象方法
*/
// console.dir(Promise)
function queryData(url) {
return new Promise(function(resolve, reject){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState != 4) return;
if(xhr.readyState == 4 && xhr.status == 200) {
// 处理正常的情况
resolve(xhr.responseText);
}else{
// 处理异常情况
reject('服务器错误');
}
};
xhr.open('get', url);
xhr.send(null);
});
}
var p1 = queryData('http://localhost:3000/a1');
var p2 = queryData('http://localhost:3000/a2');
var p3 = queryData('http://localhost:3000/a3');
Promise.all([p1,p2,p3]).then(function(result){
// all 中的参数 [p1,p2,p3] 和 返回的结果一 一对应["HELLO TOM", "HELLO JERRY", "HELLO SPIKE"]
console.log(result) //["HELLO TOM", "HELLO JERRY", "HELLO SPIKE"]
})
Promise.race([p1,p2,p3]).then(function(result){
// 由于p1执行较快,Promise的then()将获得结果'P1'。p2,p3仍在继续执行,但执行结果将被丢弃。
console.log(result) // "HELLO TOM"
})
</script>
(2)promise 的状态改变
Promise在执行时,其状态也会发生相应的改变。
peding: Promise执行时,其内部任务还未执行,.then还未拿到执行结果。(初始化时,执行器函数执行(同步执行),如果同步的直接改变状态,则可以看成一个段同步代码)
resolved:内部任务执行完毕,状态为成功。(resolved被调用时)
rejected:内部任务执行完毕,状态为失败。(rejected被调用时)
a. pending 变为 resolved
b. pending 变为 rejected
说明: 只有这 2 种, 且一个 promise 对象只能改变一次 无论变为成功还是失败, 都会有一个结果数据 成功的结果数据一般称为 vlaue, 失败的结果数据一般称为reason。
(3)promise的基本流程
(4)promise的执行顺序
当我们new一个Promise时,传入的回调函数为同步代码 ,会立即执行,而.then() .catch()里的为异步微任务。
console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
new Promise(function(resolve){ //这里的回调是同步的
console.log(3)
resolve()
}).then(function(){ //异步微任务
console.log(4)
})
console.log(5)
//输出结果 1,3,5,4,2
四、 Promise的三个缺点
1.无法取消Promise,一旦新建它就会立即执行,无法中途取消
2.如果不设置回调函数,Promise内部抛出的错误,不会反映到外部
3.当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成
注意:如果promise不通过.then或.catch方法捕获错误,在promise执行为reject状态时,会抛出一个reject错误,导致console面板报红,后面代码不在执行 ,我们也可通过配合async await 及try catch方法捕获错误执行相关逻辑操作。
try {
const res = await getArticleById(this.articleId) // getArticleById()是封装了的axios的api请求方法 本质也为一个promise
this.articleInfo = res.data
this.isLoading = false
this.previewImage()
} catch (err) {
this.isLoading = false
if (err.response && err.response.status === 404) {
this.is404 = true
}
}