一、Promise 与异步编程
1.1 异步编程的背景知识
JavaScript的引擎是基于单线程的事件循环概念构建的。
1.1.1 事件模型
通俗来讲,当用户点击某个Button,然后向任务队列添加一个任务来相应操作。这个算是最基础的异步编程,直到事件触发才执行事件。例如
let button = document.getElementById("my-btn");
button.onclick = function(){
alert("Hello World")
}
适用于简单的交互。 不适合复杂的交互,比如先点击button 然后再给button加上onclick事件,这样是不会有什么事情发生的。
1.1.2 回调模式
回调模式与事件类型相似,都会在未来某个事件点执行。却别在于被调用的函数是作为参数传入。比如nodejs,文件操作
fs.readFile("hello.txt",function(err,content){
if(err){//回调风格:错误优先
throw err
}
console.log(content)
})
console.log("world")
//输出为 world hello
如何输出 hello world 呢?
把console.log(“world”) 放到回调函数里面的console.log(content)后面。
如果我要读取后再写入呢?
fs.readFile("hello.txt",function(err,content){
if(err){//回调风格:错误优先
throw err
}
fs.writeFile("hello.txt",`${content}world`,function(err){
if(err){//回调风格:错误优先
throw err
}
})
})
如果我要频繁读写操作呢?
fs.readFile("hello.txt",function(err,content){
if(err){//回调风格:错误优先
throw err
}
fs.writeFile("hello.txt",`${content}world`,function(err){
if(err){//回调风格:错误优先
throw err
}
fs.readFile("hello.txt",function(err,content){
if(err){//回调风格:错误优先
throw err
}
fs.writeFile("hello.txt",`${content}world`,function(err){
if(err){//回调风格:错误优先
throw err
}
})
})
})
})
1.1.3 有趣的例子
- 第一关
for ( var i = 0;i < 5; i++) {
console.log(i);
}
//输出肯定是0,1,2,3,4
- 第二关
for( var i = 0;i < 5;i++) {
setTimeout(function() {
console.log(i);
},1000)
}
console.log(6)
//655555
Why? setTimout在for循环里面是异步的,人脑跑下这段代码是:
(1) 执行for 语句,把所有setTimeout记录到小本本(事件队列)里面(同步优先于异步优先于回调)
for( var i = 0;i<5;i++) {
// setTimeout(function() {
// console.log(i);
// },1000)
}
console.log(6)
(2) 所有同步代码代码执行完毕,然后执行队列里面的setTimeout
setTimeout(function() {
console.log(i);
},1000)
为什么会一次性在一秒后输出呢,别忘了 setTimeout是异步,这边他们的延时执行的任务不会互相影响,都是不会等到前面结束之后再执行。
为什么不是 1 2 3 4 5,问题出在作用域上。
(1) setTimeout 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 global。这点可以引入“变量提升”
(2)同步代码 for 循环结束,i 已经等于 5 了,这个时候再执行 setTimeout 的五个回调函数,里面的 console.log(i); 的 i 去向上找作用域。只能找到 global下 的 i,即 5。所以输出都是 5
怎么解决这个这个问题?
(1) 引入 let,不会有变量提升
for (let i = 0; i < 5; i++) { //let 代替 var
setTimeout(function (){
console.log(i);
},1000);
}
(2) 引入 闭包
for (var i = 0; i < 5; i++) {
(function(i){ //立刻执行函数
setTimeout(function (){
console.log(i);
},1000);
})(i);
}
- 第三关
setTimeout(console.log(1),0)
setTimeout(() =>console.log(2),0)
console.log(3)
let promise = new Promise((resolve,reject)=>{
console.log("4")
resolve()
})
promise.then(()=>{console.log(5)})
//13452
why :
输出1 是因为传入的参数不是方法,而是方法返回值
输出3 是因为console.log(3)同步的代码,比异步优先
输出4 是因为初始化promise对象的时候,这里边算是同步代码。
输出5 是因为,虽然是同步事件,但是Promise有自己的事件队列,优先于setTimeout的异步事件
输出2 是因为大家都输出完了
1.2 Promise基础知识
1.2.1 什么是Promise
Promise相当于一部操作的占位符,不会去订阅事件、也不会传递一个方法给目标函数,而是返回一个Promise对象。
let promise = rs.readFile("hello.txt");
promise.then(content => console.log(content))
这边文件读取的时候不会立马拿到需要的值,但是可以拿到一个Promise。
就像张三向李四讨钱
var money = liSiGetMyMoneyBack()//这时候本来期望的返回值是 money
但是李四没有钱,不能立马给张三,只能给出一个承诺,告诉张三十号领工资立马还
const liSiGetMyMoneyBack = function(){
return new Promise(resolve =>{
setTimeout(() =>{
resolve("1块钱")
},100000000000)
})
}
所以这里的返回值money是一个承诺。张三觉得李四是个很讲信用的人,所以张三可以先立一个flag,如果承诺兑现了,立马给女朋友买包包。
money.then(m =>{
console.log(`拿着钱:${m},给女朋友买包包`);
})
1.2.2 Promise的生命周期
let a = new Promise(resolve =>{})
每个promise都有一个生命周期,[[PromiseStatus]]:
- 进行中:pending()
- 成功完成:resolved(chrome,firefox是fulfilled)
- 失败:rejected
虽然chrome firefox 成功的状态值不同,但是没关系,反正我们也没办法根据PromiseStatus的值来判断promise的状态,只能通过then来处理。
1.2.3 创建未完成的Promise
promise的方法:
- then
let a = new Promise()
a.then(function(content){
console.log("RESOLVE-SUCCESS")
},function(err){
console.log("REJECT-FAIL")
})
then可以传入两个回调方法,第一个是成功时候的回调,第二个是失败的回调。
- catch
let a = new Promise()
a.catch(function(err){
console.log("REJECT-FAIL")
})
这边的catch只会有一个回调方法,就是失败的回调,相当于
let a = new Promise()
a.then(null,function(err){
console.log("REJECT-FAIL")
})
- finally/always/complete
Promise暂时没有这类的方法,不过我们可以通过垫片来处理
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
finally 必须有的特点是:
- 不接收任何参数,原来的value或者Error在finally里是收不到的
- 处理后不影响原Promise的状态,该reject还是reject,该resolve还是resolve
- 不影响Promise向后传递的传,resolve状态还是传递原来的value,reject状态还是传递原来的Error
所以用法为:
promise.then().catch().finally(()=>console.log(XXX))
注意
- 在使用Promise的时候除非链式的最后一个,否则都需要把原来的数据返回。如
var a = new Promise(resolve =>{resolve("done")})
a.then(console.info).then(console.log)//输出的是 done undefined
a.then((info)=> {console.log(info) ;return info}).then(console.info)//done done
每次调用then或者 catch的时候都会创建一个新任务,当promise被解决时候执行。这些被加到一个量身定做的单独队列中,
setTimeout(()=>console.log("setTimeout"),1000)
var a = new Promise(resolve => setTimeout(()=>{resolve("Promise")},1000))
a.then(console.log)
1.2.4 创建已完成的Promise
回到刚才的例子
var a = new Promise(resolve =>{resolve("done")})
这样的操作基本上是我们已经知道了这个Promise一定会马上返回一个状态为resolved的Promise对象。这时候我们可以使用这样的方式:
var a = Promise.resolve("done")
这样放回的Promise对象只会执行 a.then(content =>{})
如果是reject的话还能这样写:
var a = Promise.reject("fail")
同理:
这边的对象只会执行 a.catch
TIPS
- 如果Promise.resolve、 Promise.reject方法传入一个Promise对象,那么这个对象将会被直接返回
var a = new Promise(resolve => setTimeout(()=>{resolve("Promise")},1000))
var b = Promise.reject(a)
b.catch((err)=>{console.error("失败的回调")})
//log: 失败的回调
//return: Promise {<resolved>: undefined}
这里的b不是Promise {}而是返回了 a 。
- 如果Promise.resolve、 Promise.reject方法传入一个不是Promise对象而是thenable对象,会创建一个Promise对象并且在then里面被调用。
var a = {
then:(resolve) =>{
resolve("A的then")
}}
var b = Promise.resolve(a)
b.then((content)=>{console.log(content,"b的then")})
//log :A的then b的then
//return :Promise {<resolved>: undefined}
拓展Thenable对象:
当一个对象实现了then方法,那么这个而对象就是thenable对象。所有的Promise都是thenable对象(实现了then(resolve,reject)),但并非所有的thenable对象都是Promise。
let thenable = {
then:function(resolve,reject){
resolve("XXXX")
}
}
在ES6之前很多库实现了Thenable,要向下兼容转成正式的Promise就很重要了。
1.2.5 执行器错误
如果执行器内部抛出错误,那么就会进入拒绝程序就会被调用。
let promise = new Promise((resovle,reject)=>{
throw new Error("异常!")
})
promise.catch(err => console.log(err))
//log: 异常
为什么代码错了 还能继续跑呢?因为执行器内部有一个try catch,错误会被捕获并且传入拒绝处理程序。相当于:
let promise = new Promise((resovle,reject)=>{
try{
throw new Error("异常!")
}catch(e){
reject(e)
}
})
promise.catch(err => console.log(err))
//log: Error: 异常!
// at Promise (<anonymous>:3:12)
// at new Promise (<anonymous>)
// at <anonymous>:1:15
1.2.6 全局的Promise拒绝处理
在没有拒绝处理程序的情况下拒绝一个Promise,不会提示失败信息,这是JS唯一没有强制报错的地方,有些人以为是标准的缺陷。
Promise特性决定了一个Promise是否有被处理过,如
window.rejected = Promise.reject("666");
//页面加载后,这里没有被处理,
//过了一段时间,在控制台输入以下
rejected.catch(val =>{
console.log(val)
})
只要Promise对象创建,在任何时候都可以引用then catch方法,但是你不知道什么时候被处理了。
- nodejs 怎么处理
- nodejs 处理Promise拒绝的时候会触发process对象的两个事件:
- unhandledRejection 当promise失败(rejected),但又没有处理时触发
- rejectionHandled, 当promise失败(rejected),被处理时触发
- nodejs 处理Promise拒绝的时候会触发process对象的两个事件:
let rejected;
process.on("unhandledRejection",function(err,promise){
console.log(err.message)//"异常"
console.log(promise == rejected)//true
})
rejected = Promise.reject(new Error("异常"))
//注意:这里没有任何拒绝的处理
let rejected;
process.on("rejectionHandled",function(promise){
console.log(promise == rejected)//true
})
rejected = Promise.reject(new Error("异常"))
//等待添加拒绝处理的程序,如果setTimeout去掉,那么这两个监听事件就不会被执行。
setTimeout(function(){
rejected.catch(console.log)
},1000)
- 未处理拒绝跟踪器
- 原理:同步代码执行结束后(unhandledRejection),没有处理异常的Promise都放到一个Map里面,开始执行异步代码后(rejectionHandled)如果在后续被处理了,则移除这map.
let rejections = new Map();
// 当一个rejection出现,添加到map中
process.on("unhandledRejection", function(reason, promise) {
rejections.set(promise, reason);
});
// 存在handler,则将其从map集合中删除
process.on("rejectionHanled", function(promise) {
rejections.delete(promise);
});
// 间隔一段时间对集合中的查看处理,清空
setInterval(function() {
rejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// 用某种方法处理这些rejections
handleRejection(promise, reason);
});
// 清空集合
rejections.clear();
}, 60000);
-
浏览器处理
- 浏览器也是通过触发两个事件来识别未处理的拒绝,虽然这些事件是在window对象上面,但与nodejs基本一致。
-
unhandledRejection 当promise失败(rejected),但又没有处理时触发
-
rejectionHandled, 当promise失败(rejected),被处理时触发
不同的是,这些事件只有一个参数,参数包含:
-
- 浏览器也是通过触发两个事件来识别未处理的拒绝,虽然这些事件是在window对象上面,但与nodejs基本一致。
{
type:"unhandledRejection",//unhandledRejection || rejectionHandled
promise:[Promise.reject(reason)],//被拒绝的对象
reason:reason//promise的拒绝值
}
let rejected;
window.onunhandledrejection = function(event){
console.log(event.type)//"unhandledRejection"
console.log(event.reason.message)//"异常"
console.log(event.promise == rejected)//true
}
window.onrejectionhandled = function(event){
console.log(event.type)//"rejectionHandled"
console.log(event.reason.message)//"异常"
console.log(event.promise == rejected)//true
}
rejected = Promise.reject(new Error("异常"))
//注意:这里没有任何拒绝的处理
- 未处理拒绝跟踪器(基本上和node的一样的处理方式)
略。 - chrome的表现:
虽然报错了,但仅仅是用console.error的方式输出错误信息,并不会像throw一样终止下面的程序。var a = Promise.reject("ERR") console.log("继续执行") //log:继续执行 //VM192:1 Uncaught (in promise) ERR
1.2.7 串联(链式调用)Promise
每次调用then 或者 catch方法的时候,实际上又创建并返回了另一个Promise对象,只有第一个Promise执行完成或者拒绝的时候,第二个才会被解决。
- 普通串联
let promise = Promise.resolve("Hello World");
promise.then(console.log).then(console.error)
//log:hello world
//error:undefined
拆开之后:
let promise1 = Promise.resolve("Hello World");
let promise2 = promise1.then(console.log)
promise2.then(console.error)
- 捕获异常
在上述的情况下可能发生错误,这时候可以通过Promise链来捕获这些错误。例如
let promise = Promise.resolve("Hello World");
promise.then(val =>{
throw new Error("异常")
}).catch(console.error)
//error: 异常
例子二
let promise = Promise.reject("Boom");
promise.catch(val =>{
console.log(val)
throw new Error("sakalaka")
}).catch(console.error)
//log: Boom
//error: sakalaka
务必要在Promise最后面留有一个决绝处理的程序来保证能够处理所有可能发现的错误
1.2.8 Promise链式返回值
正如上面的介绍,每次调用then 或者 catch方法的时候,实际上又创建并返回了另一个Promise对象,每个返回值都是一样的吗?
还记得这个例子吗:
let promise = Promise.resolve("Hello World");
promise.then(console.log).then(console.error)
//log:hello world
//error:undefined
为什么第二个控制台输出是Undefined?因为第一个then里面传入的方法只有console.log,而console是没有返回值的,也就是创建的第二个Promise对象没有返回值,所以第二个then里面的方法就没有返回值。输出自然为undefined。
正面例子:
let promise = Promise.resolve(100);
promise.then(value => {
console.log(value);
return value+1
}).then(console.error)
//log:100
//error:101
第一个捕获错误返回值可以给下面的Promise对象的then用
let promise = Promise.reject(100);
promise.catch(value => {
console.log(value);
return value+1
}).then(console.error)
//log:100
//error:101
1.2.9 Promise链式返回Promise对象
在Promise可以通过完成和拒绝处理程序中的返回值来传递数据,如果是Promise呢?
let p1 = Promise.resolve("HELLO")
let p2 = Promise.resolve("WORLD")
p1.then(val =>{
console.log(val);//HELLO
return p2;
}).then(val =>{
console.log(val);//WORLD
})
如果上述的p2被截胡了,返回错误了怎么办?
let p1 = Promise.resolve("HELLO")
let p2 = Promise.reject("ERROR")
p1.then(val =>{
console.log(val);//HELLO
return p2;
}).then(val =>{
console.log(val);//从不执行
}).catch(val =>console.log(val))//ERROR
小小总结:链式调用,后面的Promise是then 还是 catch,取决于上一个Promise返回的对象类型。
1.2.10 响应多个Promise对象
目前为止,前面讲的都是单个Promise的响应,如果有多个Promise,那么我们应该怎么处理?
- Promise.all()方法
只接受一个数组参数,数组的内容可以是Promise对象,也可以是其他对象。一般来说,其他对象被当做Promise.resolve(Object)来使用。返回值也是一个Promise对象,当参数里面的所有Promise都被正确处理,则Promise.all([…p]) then的返回值为Promise对象数组返回值组成的新数组。如果有其中一个被拒绝则该方法返回的也是被拒绝的Promise对象。
let p1 = Promise.resolve("1");
let p2 = Promise.resolve("2");
let p3 = Promise.all([p1,p2,3]);
p3.then(console.log)//“1” “2” 3
let p1 = Promise.resolve("1");
let p2 = Promise.reject("2");
let p3 = Promise.all([p1,p2,3]);
p3.catch(console.log)//“1” “2” 3
简而言之,Promise.all()的参数都为成功的时候,才返回一个成功的状态,否则一个拒绝,则直接返回拒绝状态。
- Promise.race()方法
race就是竞赛的意思,和Promise.all()方法的的参数一样是数组,不一样的是当参数里面有一个Promise被处理或者拒绝,则返回那个最先被处理或者拒绝的对象值。
打个比方,一群人赛跑,一旦有人冲到终点则吹哨(成功执行),一旦有人摔倒同样吹哨(拒绝执行)
let p1 = Promise.resolve("1");
let p2 = Promise.resolve("2");
let p3 = Promise.race([p1,p2,3]);
p3.then(console.log)//“1”
这里P1先跑到终点,所以返回值只有P1的“1”
let p1 = Promise.reject("ERR1");
let p2 = Promise.reject("ERR2");
let p3 = Promise.race([p1,p2,3]);
p3.catch(console.log)//“ERR1”
这里的P1先摔倒,所以返回值也只有“ERR1”
1.2.11 继承Promise对象
如果我想做出一个自己想要的自定义Promise对象呢?
class MyPromise extends Promise{
success(resolve,reject){
return this.then(resolve,reject)
}
fail(reject){
return this.catch(reject)
}
always(always){
let P = this.constructor;
return this.then(
value => P.resolve(always()).then(() => value),
reason => P.resolve(always()).then(() => { throw reason })
);
}
}
1.2.12 未来的Promise
async function name([param[, param[, ... param]]]) { statements }
[return_value] = await expression;
var ajax = () => {
return new Promise(resolve =>{
setTimeout(() => {
resolve("done");
}, 2000);
})
}
async function getData(x) {
var a = await ajax();
console.log(a)
}
还记得张三讨钱的故事吗,这回张三学聪明了
const liSiGetMyMoneyBack = function(){//向李四讨钱,李四每次都承诺我有钱了就还
return new Promise(resolve =>{
setTimeout(() =>{
resolve("1块钱")
},100000000000)
})
}
async function imNotBusy(x) {//张三说我不要你的承诺,我要钱。我不急,在你家等你
let money = await liSiGetMyMoneyBack();
console.log(`拿着钱:${money},给女朋友买包包`);
}