什么是Promise?
Promise是异步编程的一种解决方案。
Promise对象表示异步操作最终的完成(或失败)以及其结果值。
Promise的特点
-
对象的状态不受外界影响。一个Promise必然处于以下几种状态之一:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已成功(fulfilled):操作成功完成。
- 已失败(rejected):操作失败结束。
-
一旦状态改变,就确定下来,不会因为再次调用而变更结果。状态确定后,后续访问该Promise对象,只能访问到变化后的状态值。这和事件不一样,事件的特点是,你一旦错过事件执行过程后再去监听,是无法获得该次事件的执行结果的。
为什么要有Promise?
- 在原型链上提供then,解决回调地狱,利用异步链的形式修改原本的回调形式,让代码结构更清晰易懂。
- 解决JavaScript中难以处理数据并发的情况,提供了all、any、race、allselected等api让数据并发能够被高效、清晰的处理掉。
- 在原型链上提供catch,解决JavaScript中难以统一管理多个回调的报错情况。
- 在原型链上提供finally,解决JavaScript中报错后难以确保必须执行回调操作被执行到位。
基本用法
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
Promise的基础实现
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise和setTimeoutout的结合使用
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
Promise内嵌Promise
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
Promise.prototype.then()
Promise实例的then方法最多接受两个参数:用于Promise成功回调与失败回调的回调函数,它立即返回一个等效的Promise对象,允许你链接到其他Promise方法,从而实现链式调用。 当then方法接收到一个参数时,默认是Promise成功回调的方法:
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
then接收到两个参数的情况:
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function (comments) {
console.log("resolved: ", comments);
}, function (err){
console.log("rejected: ", err);
});
Promise.prototype.catch()
Promise.prototype.catch接收一个参数,是指定发生错误时的回调函数,是.then(null,rejection)或.then(undefined,rejection)的别名。
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等价于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
Promise.prototype.catch与trycatch的关系
举例,当一个Promise中出现问题时,可以用catch进行处理:
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面的例子可以简写为:
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
用trycatch的形式改写上面的例子:
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
如果Promise状态已经变成resolved,再抛出错误是无效的
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代码中Promise在resolve语句后面再抛出错误,不会被捕获。
Promise错误具有冒泡性与封闭性
- 冒泡性:Promise链式调用中产生的Error总能被下一个catch捕获到
- 封闭性:Promise链式调用中产生的Error后续没有遇到catch,也只会被Promise自己处理掉,不会影响外部代码
Promise.prototype.finally
finally接收一个回调函数作为参数,并且不管Promise处于哪一个状态,都会执行参数内容。
finally的参数内容为一个不接受任何参数的回调函数,也就是说finally不能感知到Promise的状态,本质上finally也是一个then方法。
finally与then的关系
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
finally的实现原理
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
Promise.all()
Promise.all方法用于将多个Promise实例包装成一个新的Promise实例,这个Promise实例的状态变更条件和参数内的Promise实例的状态有关:
- 只有当参数内所有Promise都变成resolve状态时,all方法返回的Promise才变成resolve,返回的Promise是一个数组,数组内的元素顺序和参数的Promise顺序一致(而不是Promise列表的响应顺序),传入的Promise参数仍然支持setTimeout等异步回调
- 只要当参数内有一个Promise变成rejected状态时,all方法返回的Promise就变成reject,返回的错误就是第一个Promise所rejected抛出的错误。
Promise方法的参数规则,all方法以及下列各个方法的参数都适用这个规则
参数类型是必须具有Iterator接口,且返回的每个成员都是Promise实例的“对象”。例如:
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
常见的具有Iterator接口的类型有:Array、String、Set、Map、arguments等。
Promise.race()
Promise.race方法同样将多个Promise实例包装成一个新的Promise实例,这个实例的状态变更只和参数内首个转变状态的Promise有关:
- 当参数内有Promise转变成resolve,那么race方法的实例就变成resolve,并且返回的结果就是首个改变的Promise实例的返回值。
- 当参数内有Promise转变成reject,那么race方法的实例就变成reject,并且抛出的报错就是首个改变的Promise实例抛出的报错。
Promise.allSettled()
Promise.allSettled方法同样将多个Promise实例包装成一个新的Promise实例,这个实例的状态返回值总会是fulfilled。
当参数内所有的Promise都返回结果后,allSettled才转变状态未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 }
// ]
成员对象的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);
Promise.any
Promise.any()接受一组Promise实例作为参数,包装成一个Promise实例返回。any和all的结果取值逻辑完全相反:
- 只要有一个Promise实例调用resolve,那么any就转变为fulfilled,并返回该resolve的值。
- 当所有的Promise实例都调用reject,any才会转变为rejected状态,并返回第一个rejected的值,并依次抛出rejected的值。
Promise.resolve()
Promise可以将现有对象转为 Promise 对象。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Vue3的nextTick()与Promise.resolve()的关系
nextTick会先检测当前循环队列下是否已创建Promise实例,如果未创建,则使用Promise.resolve()方法直接创建一个已解决的Promise,从性能上讲,这样减去了创建Promise并且让Promise进入resolve的性能开销,nextTick只需要实现将任务推入微任务队列即可。
resolve的四种参数类型
- 参数是一个Promise实例:如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例
- 参数是一个thenable对象(指具有then方法的对象):Promise.resolve会将这个对象转变成Promise对象,然后立即执行它的then方法,例如:
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
- 参数不是具有then()方法的对象,或根本不是对象:Promise.resolve会直接返回一个状态未resolved的新的Promise对象。例如:
const p = Promise.resolve('Hello');
p.then(function (s) {
console.log(s)
});
// Hello
- 不带有任何参数:创建一个resolved状态的Promise对象,这也是Vue3中nextTick的实现方式的核心之一。
Promise.reject
Promise.reject(reason)方法也会返回衣蛾新的Promise实例,该实例的状态未rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
上面代码生成一个 Promise 对象的实例p
,状态为rejected
,回调函数会立即执行。
Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。
Promise.reject('出错了')
.catch(e => {
console.log(e === '出错了')
})
// true
上面代码中,Promise.reject()
方法的参数是一个字符串,后面catch()
方法的参数e
就是这个字符串。