唠唠叨叨
最近考试比较多,终于快结束了,间隙时间,我第一遍看了《你不知道的JavaScript》的上中篇,说实话,可能是因为译本的原因,也可能是知识比较深入,我感觉比较难理解,有时感觉一小节讲完,发现他的中心观点只有一句话。但总的来说,这是套值得深入研读的书籍,暑假我将再次研读。
Promise的神秘面纱
ES6的知识点着实比较庞大,除了对Number,Array等的扩展。Promise这个概念也很吸引人。比如著名的axios
就是采用Promise来封装的。
我在《你不知道的JavaScript》中看到一个比较形象的比喻,当你去买一个汉堡包,你先把钱给服务员,然后服务员会给你一个凭证,在这个汉堡包被完成之前,凭证即代表了你的汉堡包。一段时间过后,服务员通知你结果。但这是服务员可能会对你说:‘Sorry Sir,hamburger has sold out…’这种失败的结果即为Promise中的reject。但大多数情况下,我们期望并得到的结果是一个汉堡包 ( solved)。
也就是说:Promise是一个容器,里面保存着未来才会结束的事件(通常是一个一步操作)的结果。
Promise的特点
- Promise的状态不收外界的影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise的基本用法
1 | //定义promise |
第二种的好处在于,1.采用链式的写法可以捕获前一个then抛出的错误,而一then中同时写resolve与reject无法做到。2.在语法上更人性化,类似于try…catch…语法。但实际catch = then(null,rejection).
都是捕获产生的错误,包括rejected,Error;对于错误,还有一点值得注意的是,reject或者resolve即代表这个promise状态的冻结。类似于函数的return,这两个语句一旦发生,其后面所有的语句都将无效。即使是抛出错误,也无法再被catch所捕获。
Promise的链式玩法
前面的then…catch…其实已经是链式的写法了。
Promise有3个函数可以链式调用。分别是
then(func(reslved,rejected){...})
: 为Promise实例添加状态改变时的回调函数。catch(func(err){...})
: 用于指定发生错误时的回调函数。包括rejected也被他处理。finally(func(){...})
: 无论状态如何,是否抛出错误。都将执行该回调。
但其能够一直链式调用的原因是:这3个函数每次调用都会产生一个新的Promise对象,并且这个新的promise会将旧的promise返回值作为参数,这个对象又包含了这3个方法,所以可以一直链式调用。
Promise的高级玩法(模式)
- Promise.all([p1,p2,…])
说明:该方法用于将多个Promise实例包装成一个Promise实例。返回值判定方法为:
a. 所有的Promise的值变为resolved时,该对象才返回resolved,并返回所有的结果。
b. 一旦有一个Promise对象返回rejected,则整个对象返回rejected,并返回错误原因。
由Promise封装的axios中,有axios.all([re1,re2,…])方法
- Promise.race([p1,p2,…])
说明:该方法用于将多个Promise实例包装成一个Promise实例。返回值判定方法为:
a. 当第一个Promise返回resolved时,整个Promise状态变为resolved,并返回第一个Promise resolved的结果,一旦有任何一个Promise决议为拒绝,他就会拒绝。
值得注意的
- Promise.all在传入空数组时,会立即返回resolved;但Promise.race会被一直挂起。所以不要想Promise.race中传递空数组。希望ECMA会尽快修改这一点。
一些骚操作
在《你不知道JavaScript》一书中,提出来一些ES6标准之外的Promise模式之外的一些扩展模式,比如:
Promise.none([...])
:与Promise.all
的情况互换,即所有的Promise都rejected才rejected。反之则返回resolved。Promise.any([...])
:与race相对,该模式忽略rejected,得到第一个resolved。Promise.first([...]):
这个模式类似与any([…])的竞争,,即只要第一个Promise完成,他就会忽略有序的任何拒绝和完成。Promise.last([...])
:这个类似与first([…],但确实最后一个完成胜出。
示例:Promise.first([...])
的实现
//polyfill安全的guard检查
if(!Promise.first){
Promise.first = function(prs){
//返回最终的Promise对象
return new Promise(function(resolve,reject){
//把所有的Promise循环一次
prs.forEach(function(pr){
//把值规整化,并且不管哪个先完成,就决议组Promise
Promise.resolve(pr).then(resolve);
})
})
}
}
Promise的实际应用
- 异步加载图片
1 | const preloadImage = function (path) { |
异步Ajax
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
29const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
最后
Promise着实避免了回调函数的一些缺陷,并且它的使用也越来越广泛,包括后面的async异步函数,也是利用Promise来完成的,因此,这里简单的了解一下Promise还是很有必要的。
2021年1月18日补充
概念
其在新建时就开始立即执行,一旦完成,其状态就会”凝固“。
“凝固”过后其状态就不会发生改变,并且与事件不同的是,其可以随之被监听,即:
1
2
3
4
5
6
7
8
9
10
11const p1 = new Promise(resolve, reject){
setTimeout(()=>{resolve(1)}, 3000)
}
p1.then((res) => {
console.log(`${res}--第一次获取`)
})
p1.then((res) => {
console.log(`${res}--第二次获取`)
})第一个结果为:1—第一次获取
第二个结果为:1—第二次获取
注意:这两次执行都会在3秒过后执行,并且都会执行;这里区别于普通事件,一旦错过,就无法再监听到了。但是Promise在操作完成后会凝固状态,无论后面在什么时候进行监听(定义其then函数)都会被执行。
以实现的接口
Promise.all([p1, p2, p3])
,其返回结果分为两种:只有
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。注意:注意,如果作为参数的 Promise 实例,自己定义了
catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。如果p2
没有自己的catch
方法,就会调用Promise.all()
的catch
方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const 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: 报错了]
Promise.race([p1, p2, p3])
:上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态(无论是fulfilled
还是reject
),p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。下面这个即利用这个特性完成请求超时:
1
2
3
4
5
6
7
8
9
10const 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);Promise.allSettled(p1, p2, p3)
:方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。该方法返回的新的 Promise 实例,一旦结束,状态总是
fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
1
2
3
4
5
6
7
8
9
10
11
12const 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.any(p1, p2, p3)
:该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。(与all相反)1
2
3
4
5
6
7
8
9
10
11const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}Promise.any()
抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected
的操作所抛出的错误。下面是 AggregateError 的实现示例。捕捉错误时,如果不用
try...catch
结构和 await 命令,可以像下面这样写。1
2
3
4
5
6
7
8Promise.any(promises).then(
(first) => {
// Any of the promises was fulfilled.
},
(error) => {
// All of the promises were rejected.
}
);下面是一个例子。
1
2
3
4
5
6
7
8
9
10
11var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);
Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
console.log(result); // 42
});
Promise.any([rejected, alsoRejected]).catch(function (results) {
console.log(results); // [-1, Infinity]
});Promise.resolve()
:有时需要将现有对象转为 Promise 对象,Promise.resolve()
方法就起到这个作用。参数是一个 Promise 实例
如果参数是 Promise 实例,那么
Promise.resolve
将不做任何修改、原封不动地返回这个实例。参数是一个
thenable
对象thenable
对象指的是具有then
方法的对象,比如下面这个对象。1
2
3
4
5let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};Promise.resolve()
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then()
方法。1
2
3
4
5
6
7
8
9
10let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});上面代码中,
thenable
对象的then()
方法执行后,对象p1
的状态就变为resolved
,从而立即执行最后那个then()
方法指定的回调函数,输出42。参数不是具有
then()
方法的对象,或根本就不是对象如果参数是一个原始值,或者是一个不具有
then()
方法的对象,则Promise.resolve()
方法返回一个新的 Promise 对象,状态为resolved
。1
2
3
4
5
6const p = Promise.resolve('Hello');
p.then(function (s) {
console.log(s)
});
// Hello上面代码生成一个新的 Promise 对象的实例
p
。由于字符串Hello
不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved
,所以回调函数会立即执行。Promise.resolve()
方法的参数,会同时传给回调函数。Promise.resolve()
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise 对象。
Promise.reject()
:会返回一个新的 Promise 实例,该实例的状态为rejected
。1
2
3
4
5
6
7
8const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。1
2
3
4
5Promise.reject('出错了')
.catch(e => {
console.log(e === '出错了')
})
// true上面代码中,
Promise.reject()
方法的参数是一个字符串,后面catch()
方法的参数e
就是这个字符串。
目前Chrome(87)中的以实现上述方法。