本文所有JS执行结果基于Chrome浏览器83.0.4103.61版本得出,如果在其他浏览器上出现不同执行结果,那不关我事
Promise的用法示范
var p = new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/');
xhr.onreadystatechange = function() {
if (xhr.readyState === xhr.DONE)
if (xhr.status === 200)
resolve(xhr);
else
reject();
};
xhr.onerror = reject;
xhr.send();
});
p.then((xhr) => {
console.log(xhr);
return xhr.response;
}).then((resp) => {
console.log(resp);
}).catch(() => {
console.error('Failed');
}).finally(() => {
console.log('Done');
});
Promise类的构造函数的参数被称为Resolver(也叫executor),用于定义具体的异步操作,Promise对象会向Resolver函数传入resolve与reject两个参数,用于在异步操作执行成功与失败时调用。
resolve与reject两个函数都可以接受一个参数以供Promise对象后续使用。
当Resolver执行过程中出现异常时,Promise对象会自动调用reject函数。
Promise对象的状态
pending:
从Promise对象初始化到resolve或reject被调用之前,Promise对象会处于此状态。
resolved(fulfilled):
当Resolver中调用了resolve函数时,Promise会进入此状态并执行相应后续操作。
rejected:
当Resolver中调用了reject函数时,Promise会进入此状态并执行相应后续操作。
Promise对象的方法
then(onresolved, onrejected):
then是Promise对象的一个核心方法,用于定义异步操作的后续操作。
当Promise对象进入resolved状态时,它会执行onresolved参数所定义的函数。
当Promise对象进入rejected状态时,它会执行onrejected参数所定义的函数。
onresolved与onrejected两个函数都可以接受一个参数,这个参数对应Resolver向resolve与reject函数传入的参数
catch(onrejected):
catch是then的一个简写,调用catch(onrejected)等同于调用then(null, onrejected)
finally(onfinally):
finally是无论Promise对象是resolved状态还是rejected状态,都会去调用onfinally,约等于then(onfinally, onfinally),与then的区别在于onfinally不会接收参数,onfinally的return也不会传递给后面的then
等等,finally后面的then是什么鬼?
其实finally之后依然可以then、catch、finally,写起来就变成了p.finally(...).then(...).then(...)……怎么想都很奇怪,而且finally后面的then获取的参数是finally之前的返回值
Promise核心部分
上面的东西估计随便搜一下Promise就能搜着,下面的这些则是我在ES5中造轮子写Promise时研究出的一些东西。
1.
Resolver会在Promise构造函数中直接执行
验证代码:
new Promise(() => console.log(1));
console.log(2);
结果:
1
2
2.
Promise对象的链式调用then与finally返回的并不是this,而是一个根据其参数生成的新Promise对象
这些对象以单向链表的形式连接,最终会形成一棵树
后面称由then与finally生成的对象为“子对象”,被调用的对象为“主对象”,其中then的两个参数称为“状态函数”,finally方法的参数不属于状态函数
子对象刚创建时是pending状态
验证then返回不同Promise对象:
var p = new Promise(r => r());
var pp = p.then(() => {
console.log(p);
console.log(pp);
console.log(p===pp);
});
结果:
Promise {: undefined}
Promise {}
false
3.
Resolver中的参数resolve与reject一共只能调用一次,因为只有Promise对象是在pending状态时,resolve与reject才能修改Promise对象的状态与值,而只有在Promise对象的状态发生改变时,主对象才会开始调用子对象的状态函数
验证代码:
var funcs = {};
var p = new Promise((resolve, reject) => {
funcs.resolve = resolve;
funcs.reject = reject;
});
p.then(()=>console.log('res'), ()=>console.log('rej'));
console.log(p);
funcs.resolve(1);
console.log(p);
funcs.reject(2);
console.log(p);
结果
Promise {}
Promise {: 1}
Promise {: 1}
res
4.
上面的结果显示console.log('res')晚于代码中的最后一句console.log(p)执行,其实Promise与setTimeout类似,会把主对象调用子对象状态函数的过程推迟到空闲时执行,防止主线程里一堆then、finally还没执行完,甚至可能构造函数都还没执行完就开始调用子对象的情况出现
虽然上面说与setTimeout类似,但主对象调用子对象的过程永远早于同一时刻所有的setTimeout,推测setInterval也是同理
所以可以把Promise.resolve(value).then(handle)当作最高优先级的setTimeout(handle, 0, value)来使用
注:
Promise.resolve(value)可以理解为new Promise(resolve => resolve(value));
Promise.reject(value)可以理解为new Promise((resolve, reject) => reject(value));
验证代码:
setTimeout(console.log, 0, 0);
new Promise(r => r()).then(z=>console.log(1));
Promise.resolve().then(z=>console.log(2));
setTimeout(console.log, 0, 4);
console.log(5);
结果
5
1
2
0
4
5.
当子对象拥有主对象状态对应的状态函数时,主对象会直接调用此状态函数,如果成功执行,子对象状态变为resolved,值为状态函数返回值;如果抛出异常,子对象状态变为rejected,值为异常对象
如果子对象没有主对象状态对应的状态函数,则会直接继承主对象的状态与值,交由更后面的子对象处理
如果最后一个子对象执行后依然是rejected状态,浏览器会抛出带有(in promise)字样的异常
验证代码:
var p = Promise.reject(1);
var pp = p.then(()=>console.log(pp)).then(()=>console.log(pp));
pp.finally(()=>console.log(pp));
结果:
Promise {: 1}
Promise {: 1}
Uncaught (in promise) 1
验证代码:
var p = Promise.resolve(1);
var pp = p.catch(()=>console.log(pp)).catch(()=>console.log(pp));
pp.finally(()=>console.log(pp));
结果:
Promise {: 1}
Promise {: 1}
6.
为防止抛出异常干扰其他Promise对象执行,浏览器会把异常留到最后抛出
验证代码:
var p = Promise.reject();
p.then(()=>console.log(1));
p.catch(()=>console.log(2)).then(()=>console.log(3)).then(()=>console.log(4));
结果:
2
3
4
Uncaught (in promise) undefined
7.
主对象使得子对象的状态发生了改变之后,子对象也会开始准备调用其自身的子对象,同样地,它会被推迟到空闲时执行
如果把Promise对象极其子对象们构成的树看成一个单向图,会发现它的执行顺序与广度优先搜索算法BFS是一致的
验证代码:
var p = Promise.resolve();
p.then(() => {console.log(1);}).then(() => {console.log(2);});
p.then(() => {console.log(3);}).then(() => {console.log(4);});
结果:
1
3
2
4
这里需要来个难理解一点的例子加深一下记忆:
var x = function() {
var xp = Promise.resolve();
xp.then(() => {console.log(1);}).then(() => {console.log(2);});
xp.then(() => {console.log(3);}).then(() => {console.log(4);});
}
var p = Promise.resolve();
p.then(x).then(() => {console.log(5);});
p.then(() => {console.log(6);}).then(() => {console.log(7);});
它的结果为:
6
1
3
5
7
2
4
如果最后两句对调,变成这样:
var x = function() {
var xp = Promise.resolve();
xp.then(() => {console.log(1);}).then(() => {console.log(2);});
xp.then(() => {console.log(3);}).then(() => {console.log(4);});
}
var p = Promise.resolve();
p.then(() => {console.log(6);}).then(() => {console.log(7);});
p.then(x).then(() => {console.log(5);});
结果为:
6
7
1
3
5
2
4
为什么是这个顺序?请先自己思考一下。
解析:
设有这么个队列,它决定了下次要调用子对象状态函数的Promise对象,就叫它“执行队列”好了
再设执行队列中所有对象的子对象按顺序可组成一个新的队列,就叫它“子对象队列”吧
第一个例子:
第一次推迟:
刚一开始,执行队列如下:
[p]
子对象队列如下:
[then(x), then(log(6))]
开始依次执行子对象队列中对象的相应状态函数
then(x)调用了x,x执行时创建了xp,xp是resolved状态,进入执行队列
x 执行结束后,then(x)自身进入执行队列
then(log(6))输出了6,自身进入执行队列
第二次推迟:
执行队列:
[xp, then(x), then(log(6))]
子对象队列:
[then(log(1)), then(log(3)), then(log(5)), then(log(7))]
依次输出了1、3、5、7,子对象队列中的对象依次进入执行队列
第三次推迟:
执行队列:
[then(log(1)), then(log(3)), then(log(5)), then(log(7))]
子对象队列:
[then(log(2)), then(log(4))]
依次输出2、4,子对象队列中的对象依次进入执行队列
第四次推迟:
执行队列:
[then(log(2)), then(log(4))]
子对象队列是空的,执行个锤子
第二个例子也就换换顺序,不分析了
8.
如果状态函数返回一个Promise对象,那么状态函数所在的Promise对象的所有子对象都会转移至返回的对象上
验证代码:
Promise.resolve().then(() => {
console.time('Promise');
}).then(() => {
return new Promise(r => setTimeout(r, 1000, 123123));
}).then(v => {
console.timeEnd('Promise');
console.log(v);
});
结果:
Promise: 1000.989990234375ms
123123
End