本篇文章来源于https://mp.weixin.qq.com/s/tetfPizYwMtr-XlBRfZAQA,主要帮助学习者快速理解认识Promise。
Promise是ES6中的特性,现在很多前端框架像AngularJS,Vue等在HTTP请求之后都是返回的Promise处理,因此Promise是必须要掌握的一个知识点。
案例1:
Promise构造函数是同步执行的,promise.then中的函数是异步执行的。
const promise = new Promise((resolve,reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
/**运行结果**/
// => 1
// => 2
// => 4
// => 3
案例2:
const first = () => (new Promise((resolve,reject) => {
console.log(3);
let p = new Promise((resolve,reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
},0);
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
/**打印结果**/
// => 3
// => 7
// => 4
// => 1
// => 2
// => 5
解析:
第一轮事件循环,先执行宏任务,主script,new Promise立即执行,输出3,执行p这个new Promise操作,输出7,发现setTimeout,将回调函数放入下一轮任务队列(Event Quene),p的then,暂时命名为then1,放入微任务队列,且first也由then,命名为then2,放入微任务队列。执行console.log(4),输出4,宏任务执行结束。
再执行微任务,执行then1,输出1,执行thne2,输出3。
第一轮事件循环结束,开始执行第二轮。第二轮事件循环先执行宏任务里面的,也就是setTimeout的回调,输出5。resolve(6)不会生效,因为p的Promise状态一旦改变就不会再变化了。
案例3:
const promise1 = new Promise((resolve,reject) => {
setTimeout(() => {
resolve('success');
},1000);
});
const promise2 = promise1.then(() => {
throw new Error("error!!!");
});
console.log('promise1',promise1);
console.log('promise2',promise2);
setTimeout(() => {
console.log('promise1',promise1);
console.log('promise2',promise2);
},2000);
/**运行结果**/
// => promise1 Promise {<pending>}
// => promise2 Promise {<pending>}
// => Uncaught (in promise) Error: error!!! at <anonymous>
// => promise1 Promise {<resolved>: "success"}
// => promise2 Promise {<rejected>: Error: error!!! at <anonymous>}
解析:
promise有3种状态:pending、fulfilled或rejected。状态改变只能是pending->fulfilled或者pending->rejected,状态一旦改变则不能再变。上面promise2并不是promise1,而是返回的一个新的Promise实例。
案例4:
const promise = new Promise((resolve,reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then: ',res);
}).catch((err) => {
console.log('catch: ',err);
});
/**运行结果**/
// => then: success1
解析:
构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用,promise状态一旦改变则不能再变。
案例5:
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});
/**运行结果**/
// => 1
// => 2
解析:
promise可以链式调用。提起链式调用我们通常会想到通过return this实现,不过Promise并不是这样实现的。promise每次调用.then或者.catch都会返回一个新的promise,从而实现了链式调用。
案例6:
const promise = new Promise((resolve,reject) => {
setTimeout(() => {
console.log('once');
resolve('success');
},1000);
});
const start = Date.now();
promise.then((res) => {
console.log(res,Date.now() - start);
});
promise.then((res) => {
console.log(res,Date.now() - start);
});
/**运行结果**/
// => once
// => success 1004
// => success 1005
解析:
promise的.then或者.catch可以被调用多次,但是这里Promise构造函数只执行一次。或者说promise内部状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch都会直接拿到该值。
案例7:
Promise.resolve()
.then(() => {
return new Error("error!!!");
})
.then((res) => {
console.log('then: ', res);
})
.catch((err) => {
console.log('catch: ',err);
});
/**运行结果**/
// => then: Error: error!!! at <anonymous>
// 此时:Promise {<fulfilled>: undefined}
解析:
.then或者.catch中return一个error对象并不会抛出错误,所以不会被后续的.catch捕获,需要改成其中一种:
- return Promise.reject(new Error('error!!!'))
- throw new Error('error!!!')
因为返回任意一个非promise的值都会被包裹到promise对象,即return new Error('error!!!')等价于return Promise.resolve(new Error('error!!!'))。
案例8:
const promise = Promise.resolve()
.then(() => {
return promise;
})
promise.catch(console.error);
/**运行结果**/
// =>TypeError: Chaining cycle detected for promise #<Promise>
/**类似于**/
process.nextTick(function tick(){
console.log("tick");
process.nextTick(tick);
});
解析:.then或.catch返回的值不能是promise本身,否则会造成死循环。
案例9:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log);
/**运行结果**/
// => 1
解析:.then或者.catch的参数期望是函数,传入非函数则会发生值穿透。
案例10:
Promise.resolve()
.then(function success(res) {
throw new Error('error');
},function fail1(e) {
console.error('fail1: ',e);
})
.catch(function fail2(e) {
console.error('fail2: ',e);
});
/**运行结果**/
// => fail2: Error: errpr at success (<anonymous ...>)
/**也等价于**/
Promise.resolve()
.then(function success1(res) {
throw new Error('error');
},function fail1(e) {
console.error('fail1: ',e);
})
.then(function success2(res) {
},function fail2(e){
console.error('fail2: ',e);
});
解析:
.then可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。
.catch是.then第二个参数的简便写法,但是它们用法上有一点需要注意:.then的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的.catch可以捕获之前的错误。
案例11:
process.nextTick(() => {
console.log('nextTick');
});
Promise.resolve()
.then(() => {
console.log('then');
})
setImmediate(() => {
console.log('setImmediate');
});
comsole.log('end');
/**运行结果**/
// => end
// => nextTick
// => then
// => setImmediate
解析:
process.nextTick和promise.then都属于microtask,而setImmediate属于macrotask,在事件循环的check阶段执行。事件循环的每个阶段(macrotask)之间都会执行microtask,事件循环的开始会先执行一次microtask。
扩展:node.js中 的process.nextTick()和setImmediate使用案例。
案例12:
红灯3秒亮一次,绿灯2秒亮一次,黄灯1秒亮一次;如何使用Promise让三个灯不断交替重复亮灯?
function red() {
console.log("红灯");
}
function green() {
console.log("绿灯");
}
function yellow() {
console.log("黄灯");
}
let myLight = (timer,cb) =>{
return new Promise((resolve) => {
setTimeout(() => {
cb();
resolve();
},timer);
});
};
let myStep = () => {
Promise.resolve().then(() => {
return myLight(3000,red);
})
.then(() => {
return myLight(2000,green);
})
.then(() => {
return myLight(1000,yellow);
}).then(()=>{
myStep();
});
};
myStep();
解析思路:
换句话说,就是红灯亮起时,承诺2秒后亮绿灯,绿灯亮起时承诺1s秒后亮黄灯,黄灯亮起时,承诺3秒后亮红灯......这显然是一个Promise链式调用,看到这里你心里或许就有思路了,我们需要将我们的每一个亮灯动作写在then()方法中,同时返回一个新的Promise,并将其状态由pending设置为fulfilled,允许下一盏灯亮起。
案例13:
const timeout = ms => new Promise((resolve,reject) => {
setTimeout(() => {
resolve();
},ms);
});
const ajax1 = () => timeout(2000).then(() => {
console.log("1");
return 1;
});
const ajax2 = () => timeout(1000).then(() => {
console.log("2");
return 2;
});
const ajax3 = () => timeout(2000).then(() => {
console.log("3");
return 3;
});
const mergePromise = ajaxArray => {
// 在这里实现你的代码
// 保存数组中的函数执行后的结果
var data = [];
// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的Promise对象
var sequence = Promise.resolve();
ajaxArray.forEach(item => {
// 第一次的then方法用来执行数组中的每个函数
// 第二次的then方法接收数组中的函数执行后返回的结果
// 并把结果添加到data中,然后把data返回
sequence = sequence.then(item).then(res => {
data.push(res);
return data;
});
});
// 遍历结束后,返回一个Promise,也就是sequence,他的[[PromiseValue]]值就是data,
// 而data(保存数组中的函数执行后的结果)也会作为参数,传入下次调用的then方法中。
return sequence;
};
mergePromise([ajax1,ajax2,ajax3]).then(data => {
console.log("done");
console.log(data); // data为[1,2,3]
});
/**要求分别输出**/
// => 1
// => 2
// => 3
// => done
// => [1,2,3]
案例14:
现有8个图片资源的url,已经存储在数组urls中,且已有一个函数function loading,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。
要求:任何时刻同时下载的链接数量不可以超过3个。
var urls = ['images/img1.jpg','images/img2.jpg',......]; // 省略了
function loadImg(url){
return new Promise((resolve,reject) =>{
const img = new Image();
img.onload = () => {
console.log("一张图片加载完成");
resolve();
}
img.onerror = reject;
img.src = url;
});
}
/**实现**/
function limitLoad(urls,handler,limit) {
// 对数组做一个拷贝
const sequence = [...urls];
let promises = [];
// 并发请求到最大数
promises = sequence.splice(0,limit).map((url,index) => {
// 这里返回的index是任务在promises的脚标,用于在Promise.race之后找到完成的任务脚标
return handler(url).then(() => {
return index;
});
});
// 利用数组的reduce方法来以队列的形式执行
return sequence.reduce((last,url,currentIndex) => {
return last.then(() => {
// 返回最快改变状态的Promise
return Promise.race(promises);
}).catch(err =>{
// 这里的catch不仅用来捕获前面的then方法抛出的错误
// 更重要的是防止中断整个链式调用
console.error(err);
}).then((res) => {
// 用新的Promise替换掉最快改变状态的Promise
promises[res] = handler(sequence[currentIndex]).then(()=>{
return res;
});
});
},Promise.resolve()).then(() =>{
return Promise.all(promises);
});
}
limitload(urls,loadImg,3);
// 因为limitLoad函数也返回一个Promise,所以当所有图片加载完成后,可以继续链式调用
limitLoad(urls,loadImg,3).then(() =>{
console.log("所有图片加载完成");
}).catch(err =>{
console.log(err);
});
解析:
用Promise来实现就是,先并发请求3个图片资源,这样可以得到3个Promise,组成一个数组promises,然后不断调用Promise.race来返回最快改变形状的Promise,然后从数组promises中删除这个Promise对象,再加入一个新的Promise。直到全部的url被取完,最后再使用Promise.all来处理一遍数组promises中没有改变状态的Promise。
案例15:
封装一个异步加载图片的方法。
function loadImageAsync(url){
return new Promise(function(resolve,reject) {
var image = new Image();
image.onload = function(){
resolve(image);
};
image.onerror = function(){
reject(new Error("Could not load at "+url));
};
image.src = url;
});
}