Promise
在JavaScript的世界中,所有代码都是单线程执行的。为了使程序不阻塞执行有了异步(I/O操作、事件操作),但是异步也有其不好之处,例如:异步回调callback回调地狱的问题,伴随着这些问题有了解决方案Promise。
面试指南
- Promise 中 .then 的第二参数与 .catch 有什么区别?,参考:错误捕获
- 怎么让一个函数无论promise对象成功和失败都能被调用?,参考:finally
promise的基本使用和原理
- 如何异常捕获(Error、reject)通过catch捕获
- 多个串联-链式执行的好处
- Promise.all和Promise.race
- Promise标准-状态变化(Pending —— Fulfilled/Rejected)
- then函数,不明文指定返回实例,返回本身的promise实例,否则返回指定的promise实例
callback方式书写
回调函数方式书写,如果异步请求多了,将会很难维护,程序看着很乱,最终会导致回调地狱。
1 | { |
promise方式书写
- resove:执行下一步操作
- reject:中断当前操作
- then:是Promise返回的对象,执行下一个,如果有两个函数,第一个表示resolved(已成功),第二个表示rejected(已失败)
1
2
3
4
5
6
7
8
9
10
11
12let ajax = function(){
console.log('promise','执行');
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
},1000);
});
}
ajax().then(function(){
console.log('promise','执行ajax方法');
});
- 执行两个Promise的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
let ajax = function(){
console.log('promise','执行');
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
},1000);
});
}
ajax()
.then(function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve();
},1000);
});
})
.then(function(){
console.log('promise3','执行3');
})
} - 多个Promise实例实现串行操作
执行a b c d 如果中间出了错误使用catch来捕获
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{
let ajax = function(num){
console.log('执行4');
return new Promise(function(resolve,reject){
if (num > 5) {
resolve();
}else{
throw new Error('出错了')
}
});
}
ajax(6).then(function(){
console.log('log','6');
}).catch(function(err){
console.log('catch',err);
});
ajax(3).then(function(){
console.log('log','3');
}).catch(function(err){
console.log('catch','err');
});
// 输出:
// 执行4
// 执行4
// log 6
// catch err
}
finally
finally() 方法返回一个Promise,在promise执行结束时,无论结果是fulfilled或者是rejected,在执行then()和catch()后,都会执行finally指定的回调函数。这为指定执行完promise后,无论结果是fulfilled还是rejected都需要执行的代码提供了一种方式,避免同样的语句需要在then()和catch()中各写一次的情况。
1 | Promise.resolve('success').then(result => { |
promise并行执行
- Promise.all()
Promise.all是将多个Promise实例当成一个Promise实例,all方法里是一个数组,数组传进来多个Promise实例,当多个Promise实例状态发生改变的时候,这个新的Promise实例才会发生变化。
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//所有图片加载完在添加到页面上
function loadImg(src){
return new Promise((resolve,reject) => {
let img = document.createElement('img');
img.src = src;
img.onload = () => {
resolve(img);
}
img.onerror = (err) => {
reject(err)
}
})
}
function showImgs(imgs){
imgs.forEach(function(img){
document.body.appendChild(img)
})
}
// 每个loadImg()方法都是一个Promise实例只有当三个都发生该变化,才会执行新的Promise实例既Promise.all()
Promise.all([
loadImg('http://www.qzfweb.com/uploads/20170512190539489.jpeg'),
loadImg('http://www.qzfweb.com/uploads/20170225143135972.jpg'),
loadImg('http://www.qzfweb.com/uploads/20170217225453679.jpg')
]).then(showImgs)
promise率先执行
- Promise.race只要其中一个实例率先发生改变,Promise.race实例也将发生改变,其他的将不在响应。
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{
// 有一个图片加载完就添加到页面上
function loadImg(src){
return new Promise((resolve,reject) => {
let img = document.createElement('img');
img.src = src;
img.onload = () => {
resolve(img);
}
img.onerror = (err) => {
reject(err)
}
})
}
function showImgs(img){
let p = document.createElement('p');
p.appendChild(img);
document.body.appendChild(p);
}
Promise.race([
loadImg('http://www.qzfweb.com/uploads/20170512190539489.jpeg'),
loadImg('http://www.qzfweb.com/uploads/20170225143135972.jpg'),
loadImg('http://www.qzfweb.com/uploads/20170217225453679.jpg')
]).then(showImgs)
}
错误捕获
Promise.then第二个参数与catch捕获错误的区别?
- .then第二参数捕获错误
.then第二个回调参数捕获错误具有就近的原则,不会影响后续then的进行。
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
29
30
31
32
33{
const ajax = function(){
console.log('promise开始执行');
return new Promise(function(resolve,reject){
setTimeout(function(){
reject(`There's a mistake`);
},1000);
});
}
ajax()
.then(function(){
console.log('then1');
return Promise.resolve();
}, err => {
console.log('then1里面捕获的err: ', err);
})
.then(function(){
console.log('then2');
return Promise.reject(`There's a then mistake`);
})
.catch(err => {
console.log('catch里面捕获的err: ', err);
})
// 输出
// promise开始执行
// then1里面捕获的err: There's a mistake
// then2
// catch里面捕获的err: There's a then mistake
} - catch捕获错误
Promise抛错具有冒泡机制,能够不断传递,可以使用catch统一处理,下面代码中不会输出then1 then2会跳过,直接执行catch处理错误
总结: 不论是Promise还是async/await在写法上解决了异步回调的问题,但是任何写法都不会改变JS单线程、异步的本质,除非js执行引擎发生变化。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
29{
const ajax = function(){
console.log('promise开始执行');
return new Promise(function(resolve,reject){
setTimeout(function(){
reject(`There's a mistake`);
},1000);
});
}
ajax()
.then(function(){
console.log('then1');
return Promise.resolve();
})
.then(function(){
console.log('then2');
return Promise.reject(`There's a then mistake`);
})
.catch(err => {
console.log('catch里面捕获的err: ', err);
})
// 输出
// promise开始执行
// catch里面捕获的err: There's a then mistake
}
手写 Promise 代码
这是一个经典的面试问题了,我将它放了最后,不废话直接上代码,共分为 5 部份完成,实现思路如下,理清了 Promise 的实现原理,很多问题自然就迎刃而解了。
- 声明 MayJunPromise 类
主要在构造函数里做一些初始化操作
- 行 {1} 初始化一些默认值,Promise 的状态、成功时的 value、失败时的原因
- 行 {2} onResolvedCallbacks 用于一些异步处理 const p = new Promise(resolve => { setTimeout(function(){ resolve(1) }, 5000) }),当 resolve 在 setTimeout 里时,我们调用 p.then() 此时的状态为 pending,因此我们需要一个地方来保存,此处就是用于保存 Promise resolve 时的回调函数集合
- 行 {3} onRejectedCallbacks 与行 {2} 同理,保存 Promise reject 回调函数集合
- 行 {4} 成功时回调,先进行状态判断是不可逆的,如果 status = pending 修改状态和成功时的 value
- 行 {5} 失败时回调,与上面行 {4} 同理,例如 resolve(1); reject(‘err’); 第二个 reject 就无法覆盖
- 行 {6} 自执行
- 行 {7} 运行失败错误捕获
1 | /** |
- Then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因
行 {8} onFulfilled、onRejected 这两个参数可选,由于 Promise .then 是可以链式调用的,对于值穿透的场景要做判断,如果不传,则返回一个函数,也就是将上个结果进行传递
行 {9} then 方法必须返回一个 promise 对象
行 {10}、{11} 、{12} 也是 then 方法内实现的三种情况,相类似,次数只拿状态等于 fulfilled 进行说明
行 {10.1} Promise/A+ 规范定义:要确保 onFulfilled、onRejected 在下一轮事件循环中被调用,你可以使用 setTimeout 来实现,因为我这里是在 Node.js 环境下,因此推荐使用了 setImmediate 来注册事件(因为可以避免掉 setTimeout 的延迟)
行 {10.2} Promise/A+ 标准规定:如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以 [[Resolve]](promise2, x) 处理解析,我们定义解析的函数 resolveMayJunPromise,也是一个核心函数,下面进行讲解 ```javascript /**
封装一个自己的 Promise
/ class MayJunPromise { …
一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因
@param { Function } onFulfilled 可选,如果是一个函数一定是在状态为 fulfilled 后调用,并接受一个参数 value
@param { Function } onRejected 可选,如果是一个函数一定是在状态为 rejected 后调用,并接受一个参数 reason
@returns { Promise } 返回值必须为 Promise
/ then(onFulfilled, onRejected) { // {8} 值穿透,把 then 的默认值向后传递,因为标准规定 onFulfilled、onRejected 是可选参数 // 场景:new Promise(resolve => resolve(1)).then().then(value => console.log(value)); onFulfilled = Object.prototype.toString.call(onFulfilled) === ‘[object Function]’ ? onFulfilled : function(value) {return value}; onRejected = Object.prototype.toString.call(onRejected) === ‘[object Function]’ ? onRejected : function(reason) {throw reason};
// {9} then 方法必须返回一个 promise 对象 const promise2 = new MayJunPromise((resolve, reject) => { // {10} if (this.status === ‘fulfilled’) { // 这里的 this 会继承外层上下文绑定的 this
// {10.1} Promise/A+ 规定:确保 onFulfilled、onRejected 在下一轮事件循环中被调用
// 可以使用宏任务 (setTimeout、setImmediate) 或微任务(MutationObsever、process.nextTick)1
2
3
4
5
6
7
8
9
10setImmediate(() => {
try {
// {10.2} Promise/A+ 标准规定:如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以 [[Resolve]](promise2, x) 处理解析
const x = onFulfilled(this.value);
// 这里定义解析 x 的函数为 resolveMayJunPromise
resolveMayJunPromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});}
// {11} if (this.status === ‘rejected’) {1
2
3
4
5
6
7
8setImmediate(() => {
try {
const x = onRejected(this.reason)
resolveMayJunPromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});}
// {12} // 有些情况无法及时获取到状态,初始值仍是 pending,例如: // return new Promise(resolve => { setTimeout(function() { resolve(1) }, 5000) }) // .then(result => { console.log(result) }) if (this.status === ‘pending’) {1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21this.onResolvedCallbacks.push(() => {
setImmediate(() => {
try {
const x = onFulfilled(this.value);
resolveMayJunPromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
this.onRejectedCallbacks.push(() => {
setImmediate(() => {
try {
const x = onRejected(this.reason)
resolveMayJunPromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});} });
return promise2; } }
- Promise 解决过程
声明函数 resolveMayJunPromise(),Promise 解决过程
是一个抽象的操作,在这里可以做到与系统的 Promise 或一些遵循 Promise/A+ 规范的 Promise 实现相互交互,以下代码建议跟随 Promise/A+ 规范进行阅读,规范上面也写的很清楚。
注意:在实际编码测试过程中规范 [2.3.2] 样写还是有点问题,你要根据其它的 Promise 的状态值进行判断,此处注释掉了,建议使用 [2.3.3] 也是可以兼容的 。
1 | /** |
- 验证你的 Promise 是否正确
Promise 提供了一个测试脚本,进行正确性验证。
同时需要暴露出一个 deferred 方法。
1 | MayJunPromise.defer = MayJunPromise.deferred = function () { |
- catch、resolve、reject、all、race 方法实现
Promise/A+ 规范中只提供了 then 方法,但是我们使用的 catch、Promise.all、Promise.race 等都可以在 then 方法的基础上进行实现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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62class MayJunPromise {
constructor(fn){...}
then(){...},
/**
* 捕获错误
* @param { Function } onRejected
*/
catch(onRejected) {
return this.then(undefined, onRejected);
}
}
/**
* 仅返回成功态,即 status = fulfilled
*/
MayJunPromise.resolve = function(value) {
return (value instanceof Promise || value instanceof MayJunPromise) ? value // 如果是 Promise 实例直接返回
: new MayJunPromise(resolve => resolve(value));
}
/**
* 仅返回失败态,即 status = rejected
*/
MayJunPromise.reject = function(value) {
return (value instanceof Promise || value instanceof MayJunPromise) ? value : new MayJunPromise(reject => reject(value));
}
/**
* MayJunPromise.all() 并行执行
* @param { Array } arr
* @returns { Array }
*/
MayJunPromise.all = function(arr) {
return new MayJunPromise((resolve, reject) => {
const length = arr.length;
let results = []; // 保存执行结果
let count = 0; // 计数器
for (let i=0; i<length; i++) {
MayJunPromise.resolve(arr[i]).then(res => {
results[i] = res;
count++;
if (count === length) { // 全部都变为 fulfilled 之后结束
resolve(results);
}
}, err => reject(err)); // 只要有一个失败,就将失败结果返回
}
});
}
/**
* MayJunPromise.race() 率先执行,只要一个执行完毕就返回结果;
*/
MayJunPromise.race = function(arr) {
return new MayJunPromise((resolve, reject) => {
for (let i=0; i<arr.length; i++) {
MayJunPromise.resolve(arr[i])
.then(result => resolve(result), err => reject(err));
}
})
} - 并发请求控制
Promise.all 同时将请求发出,假设我现在有上万条请求,势必会造成服务器的压力,如果我想限制在最大并发 100 该怎么做?例如,在 Chrome 浏览器中就有这样的限制,Chrome 中每次最大并发链接为 6 个,其余的链接需要等待其中任一个完成,才能得到执行,下面定义 allByLimit 方法实现类似功能。测试,定义一个 sleep 睡眠函数,模拟延迟执行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
29
30
31
32
33
34
35
36
37
38
39
40
41/**
* 并发请求限制
* @param { Array } arr 并发请求的数组
* @param { Number } limit 并发限制数
*/
MayJunPromise.allByLimit = function(arr, limit) {
const length = arr.length;
const requestQueue = [];
const results = [];
let index = 0;
return new MayJunPromise((resolve, reject) => {
const requestHandler = function() {
console.log('Request start ', index);
const request = arr[index].then(res => res, err => {
console.log('Error', err);
return err;
}).then(res => {
console.log('Number of concurrent requests', requestQueue.length)
const count = results.push(res); // 保存所有的结果
requestQueue.shift(); // 每完成一个就从请求队列里剔除一个
if (count === length) { // 所有请求结束,返回结果
resolve(results);
} else if (count < length && index < length - 1) {
++index;
requestHandler(); // 继续下一个请求
}
});
if (requestQueue.push(request) < limit) {
++index;
requestHandler();
}
};
requestHandler()
});
}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
29
30
31
32
33
34
35
36
37/**
* 睡眠函数
* @param { Number } ms 延迟时间|毫秒
* @param { Boolean } flag 默认 false,若为 true 返回 reject 测试失败情况
*/
const sleep = (ms=0, flag=false) => new Promise((resolve, reject) => setTimeout(() => {
if (flag) {
reject('Reject ' + ms);
} else {
resolve(ms);
}
}, ms));
MayJunPromise.allByLimit([
sleep(1000),
sleep(1000),
sleep(1000),
sleep(5000, true),
sleep(10000),
], 3).then(res => {
console.log(res);
})
// 以下为运行结果
Request start 0
Request start 1
Request start 2
Number of concurrent requests 3
Request start 3
Number of concurrent requests 3
Request start 4
Number of concurrent requests 3
Error Reject 5000
Number of concurrent requests 2
Number of concurrent requests 1
[ 1000, 1000, 1000, 'Reject 5000', 10000 ] - Promise reference