话不多说,进入正题
Promise的特点
Promise包括pending(进行中)、fulfilled(已成功)和rejected(已失败)这三种状态, 初始状态为pending, 一旦变为fullfilled或者rejected, 这种改变不可逆转, 一切取决于操作的结果. 每一次链式回调返回的都是一个Promise实例.
与事件的区别
Event: 如果你错过了Event, 再去监听, 是得不到结果的. Promise: 如果改变已经发生了, 你再对Promise对象添加回调函数, 也会立即得到这个结果.
Promise的缺点
从时间处理的先后顺序上来看, 有以下缺点
- 无法中途取消
- Promise会"吃掉错误", 如果不设置catch方法进行处理, 会中止代码执行
- catch仅能处理Promise链式调用上面在它前面的错误, 后面的错误无法捕获
- then方法有两个回调函数分别为successCallback和errorCallback, 如果第二个函数抛出了错误, 即使在后面的调用链上使用catch, 也捕捉不到错误
- pending状态不确定性: 可能在刚刚开始, 也可能即将完成
面试官问:请实现一个sleep函数
function sleep(times) {
return new Promise((resolve, reject) => {
// 从第三个参数开始, 依次为传递给第一个参数resolve函数的形参
setTimeout(resolve, times, 'done')
}).then(
(res) => {// 睡醒后干的事}
)
}
sleep(1000).then((value) => {
console.log(value);
})
Promise 新建后就会立即执行
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为微任务, 先塞到微任务队列中,等待所有同步任务执行完成之后, 在事件循环的末尾, 再一次性排空执行完成所有微任务队列中的任务, 所以再次输出Hi, 最后输出resolved
注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
先输出2, 再输出1, 道理同上
最佳实践
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
注意:then()方法指定的回调函数,如果运行中抛出错误,也会被后续的调用链式上的catch()方法捕获。
getJSON('/posts.json').then(function(posts) {
// 运行中抛出错误
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
注意:reject(new Error(reason))等同于throw new Error(reason).很好理解,就不举例了
注意:Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。一旦捕获,中止冒泡。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应,也就是说会报错误,但不会中止代码执行
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
}).catch(e => console.log(e));
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
解释:上面代码中,someAsyncThing()函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
正解
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
}).catch(e => console.log(e));
setTimeout(() => { console.log(123) }, 2000);
Promise.prototype.finally
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。 finally本质上是then方法的特例。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
它的实现也很简单。
Promise.prototype.finally = callback => {
let then = this.then
// 等同于 let then = Promise.prototype.then
return then(value => Promise.resolve(callback()).then(() => value), reason => Promise.resolve(callback()).then(() => {throw reason}))
}
解释:最后面加上的then回调函数是为了保证Promise后续的链式回调能正常使用
上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。
从上面的实现还可以看到,finally方法总是会返回原来的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
Promise.protoype.all
注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法,就会调用Promis
e.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
Promise.race
顾名思义, race: 赛跑
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
下面是一个例子,如果指定时间内没有获得结果,就将 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方法指定的回调函数。
利用这个特性可以做接口超时应急处理
面试官问:设计实现一个Promise.race?
Promise.race = promises => new Promise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve).catch(reject)
})
})
Promise.allSettled
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失.
应用:因此可以对特定的N个请求做loading关联.
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。
const resolved = Promise.resolve(111);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 111 },
// { status: 'rejected', reason: -1 }
// ]
上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有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.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。这个时候使用Promise.all就不合适了,因为你不能确保所有的请求都已经执行完成,有可能只有一个请求失败为rejected。
有这样一种场景:后台管理系统点击一个按钮之后,需要调用N个请求,同时马上禁用这个按钮;当N个请求都完成之后,不管成功失败与否,都要放开这个按钮的使用。
麻烦的写法:
const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x));
try {
await Promise.all(requests);
console.log('所有请求都成功。');
} catch {
console.log('至少一个请求失败,其他请求可能还没结束。');
}
解释:上述代码如果所有的请求都执行成功就会打印"所有请求都成功。", 只要有一个请求失败抛出错误,就会被try捕捉到进而中止try代码块中下面的代码执行,而执行catch语句,这个时候回打印"至少一个请求失败,其他请求可能还没结束。" 同样,Promise.all()无法确定所有请求都结束,有了Promise.allSettled(),这就很容易了。
面试官问:设计实现一个Promise.allSettled?
Promise.allSettled = async promises => new Promise((resolve, reject) => {
let _promises = [];
await promise.forEach(promise =>
promise.then(value => {_promises.push({status: 'fulfilled', value})}).catch(reason => {_promises.push({status:
'rejected', reason})}))
resolve(_promises)
})
Promise.any对照Promise.all,它两刚好相反,略过
不积跬步,无以至千里,不积小流,无以成江海.
每天进步一点,看见不一样的自己!