js Promise
简介
Promise是js里一种异步编程解决方案,比较传统的是回调函数和事件,熟悉nodejs的都知道,在Promise还没出现之前,一切异步编程都是通过回调函数来实现的,包括文件读取,数据库操作等,很容易造成回调地狱,而Promise可以帮我们很好的解决这个问题。
Promise可以看做是一个容器,里面存放着未来某个时间才会结束的异步操作,其内部有三种状态,分别是pending(进行中),fulfilled(已成功)和rejected(已失败)。只有内部异步操作的结果,会影响这三种状态,其他任何操作都无法影响这三种状态。
Promise对象的状态改变,只可能有两种可能:pending -> fulfilled或者pending -> rejected。只要状态改变了,就不会再改变了。
基本使用
Promise是一个构造函数,用来生成Promise实例,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己指定。如果Promise内部的异步操作成功,则调用resolve函数,把Promise内部的状态由pending变为fulfilled,并把结果作为参数抛出去,否则调用reject函数把Promise内部的状态由pending变为rejected,把错误作为参数抛出去。
const fs = require('fs');
// 1、创建promise对象,一经创建,立马执行
new Promise((resolve, reject) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
if (err) {
reject(err);
} else {
// 请求成功时,调用.then()里面自己写的resolve函数
resolve(data);
}
});
})
抛出去的结果我们可以使用Promise的then方法进行接收,Promise的then方法接收两个回调函数作为参数,第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
const fs = require('fs');
// 1、创建promise对象,一经创建,立马执行
new Promise((resolve, reject) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then(
// 下面这个函数式作为上面Promise的resolve使用
(data) => { // 这里的参数就是上面Promise里resolve传出来的参数
console.log(data.toString());
},
// 这个作为reject使用
(err) => {
console.log(err)
}
)
使用Promise的注意点
- 调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即resolved的Promise是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说一旦Promise内部调用resolve或者reject之后,它的任务就完成了,不应该继续在后面执行其他任务,因此我们最好再resolve和reject前面加上return
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
链式调用
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
const fs = require('fs');
// then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
// 第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
new Promise((resolve, reject) => {
fs.readFile('./data/a.txt', (err, data) => {
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
.then((data) => {
console.log(data.toString());
return '在.then()中返回的结果,会传入下一个then的参数中'
})
.then((data) => {
console.log(data);
return 1
})
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log('err', err);
})
输出
aaaaaaaaaaaaaaaaaaaaaa // 文件a.txt的内容
在.then()中返回的结果,会传入下一个then的参数中
1
Promise封装
假如我们要依次地区文件的内容,我们可能会像下面这样编码
const fs = require('fs');
new Promise((resolve, reject) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then((data) => {
console.log(data.toString());
return new Promise((resolve, reject) => {
fs.readFile(__dirname + '/data/b.txt', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
})
}).then((data) => {
console.log(data.toString());
return new Promise((resolve, reject) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
})
}).then((data) => {
console.log(data.toString());
});
这样写没问题,但是充斥了大量的重复代码,因此我们可以对读取文件的Promise进行一层封装,改造成以下代码
const fs = require('fs');
function readFiles(...args) {
return new Promise((resolve, reject) => {
fs.readFile(...args, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
})
}
readFiles(__dirname + '/data/a.txt', 'utf8')
.then((data) => {
console.log(data);
return readFiles(__dirname + '/data/b.txt', 'utf8');
})
.then((data) => {
console.log(data);
return readFiles(__dirname + '/data/c.txt', 'utf8');
})
.then((data) => {
console.log(data);
return readFiles(__dirname + '/data/a.txt', 'utf8');
})
.then((data)=>{
console.log(data);
});
我们封装了一个readFiles函数,返回一个读取文件的Promise对象,在链式调用中,then函数返回的也是一个Promise对象,因此可以一层一层往下调用
错误处理
Promise的错误处理有两种方式,一种是在then函数的第二个回调函数参数中进行处理,另一种是使用Promise.catch进行错误捕获。
例如:
let p = new Promise(...)
p.then(null, reject => console.log(reject))
或者
let p = new Promise(...)
p.then(resolve => console.log(resolve))
.catch(err => console.log(err))
Promise.catch具有”冒泡”性质,意思是无论then链式调用了多少层,Promise.catch都能捕获到其中任意一层发生的错误
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
因此我们推荐总是使用Promise.catch进行异常捕获
finally
不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的Promise状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于Promise的执行结果。
const fs = require('fs');
const P1 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
resolve(data)
})
});
const P2 = new Promise((resolve) => {
throw new Error('读取文件b.txt出错了')
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.all([P1, P2, P3])
.then(res => {
console.log(res);
res.map(file => console.log(file.toString()))
})
.catch(err => console.log(err))
.finally(() => console.log('文件读取完毕'))
Promise.all
Promise.all可以把多个Promise实例,组装成一个Promise实例,此时只有当所有的Promise的状态都变为fulfilled时,Promise.all实例的状态才会变为fulfilled,只要其中有一个实例状态变为了rejected,最终Promise.all实例的状态就会变成rejected。
Promise.all我们常用来并发的执行异步操作,比如我们想要同时读取三个文件,使用Promise链式调用会逐一读取,会造成一定的性能损失。
Promise.all接收一个Promise的数组(不一定是数组,只要有iterator接口就行)作为参数,返回包装后的Promise实例,实例的then方法接收的结果是所有Promise实例结果的数组。无论异步操作哪个先执行完毕,结果数组的顺序和Promise.all传入的Promise实例数组的顺序相同
再次强调一下,
Promise.all入参不一定是数组,只要有iterator接口就行
const fs = require('fs');
const P1 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
resolve(data)
})
});
const P2 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/b.txt', (err, data) => {
resolve(data)
})
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.all([P1, P2, P3]).then(res => {
console.log(res);
res.map(file => console.log(file.toString()))
})
输出
[
<Buffer 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61>,
<Buffer 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62>,
<Buffer 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63>
]
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccc
注意,如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const fs = require('fs');
const P1 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
resolve(data)
})
});
const P2 = new Promise((resolve) => {
throw new Error('读取文件b.txt出错了')
}).catch(err => err);
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.all([P1, P2, P3])
.then(res => {
console.log(res);
res.map(file => console.log(file.toString()))
})
.catch(err => console.log(err))
结果
aaaaaaaaaaaaaaaaaaaaaa
Error: 读取文件b.txt出错了
cccccccccccccccccccccc
如果P2没有自己的catch方法,错误则会被Promise.all捕获
const fs = require('fs');
const P1 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
resolve(data)
})
});
const P2 = new Promise((resolve) => {
throw new Error('读取文件b.txt出错了')
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.all([P1, P2, P3])
.then(res => {
console.log(res);
res.map(file => console.log(file.toString()))
})
.catch(err => console.log(err))
结果报错
Promise.race
Promise.race,顾名思义,race是竞赛的意思,Promise.race里包装的Promise实例,只要有一个实例的状态率先改变了,那么Promise.race的状态就随之改变,那个率先改变状态的返回值,将作为Promise.race``then函数的入参,无论那个实力是成功或者失败,Promise.race的状态都会随之改变
const fs = require('fs');
const P1 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/a.txt', (err, data) => {
resolve(data)
})
});
const P2 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/b.txt', (err, data) => {
resolve(data)
})
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.race([P1, P2, P3]).then(res => {
console.log(res.toString())
}).catch(err => console.log(err));
Promise.race额错误处理与Promise.all有着相同的特性,如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,就不会触发Promise.race()的catch方法。
Promise.allSettled
只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束,和前面的方法不同的是,Promise.allSettled的返回值,不是结果数组,而是一个{status: 'fulfilled' | 'rejected', [reason|value]: VALUE}对象的数组,如果status是fulfilled,表示该Promise实例成功,那么第二个属性是value,值是异步操作的结果,如果status是rejected,表示该Promise实例有异常,那么第二个属性是reason,值是异常信息。
const fs = require('fs');
const P1 = new Promise((resolve) => {
throw new Error('错了')
});
const P2 = new Promise((resolve) => {
throw new Error('错了')
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.allSettled([P1, P2, P3]).then(res => {
console.log(res)
}).catch(err => console.log(err));
结果
[
{
status: 'rejected',
reason: Error: 错了
at C:\Users\lyucan\Desktop\pro\newRepo\Repositories\myrepo\nodejs\Promise
...
},
{
status: 'rejected',
reason: Error: 错了
at C:\Users\lyucan\Desktop\pro\newRepo\Repositories\myrepo\nodejs\Promise
...
},
{
status: 'fulfilled',
value: <Buffer 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63>
}
]
往往我们需要自己手动筛选出成功的结果或失败的结果,根据需要自行处理
Promise.any
只要参数实例有任何一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise变成rejected状态而结束。
const fs = require('fs');
const Promise = require("bluebird");
const P1 = new Promise((resolve) => {
throw new Error('错了')
});
const P2 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/b.txt', (err, data) => {
resolve(data)
})
});
const P3 = new Promise((resolve) => {
fs.readFile(__dirname + '/data/c.txt', (err, data) => {
resolve(data)
})
});
Promise.any([P1, P2, P3]).then(res => {
console.log(res.toString())
}).catch(err => console.log(err));
结果
bbbbbbbbbbbbbbbbbbbbbb
如果
c.txt文件优先读取完毕,那么结果就是c.txt文件的内容
需要注意的是,Promise.any还只是草案里的特性,不能直接使用,我们可以引用社区的Promise库bluebird来使用
Promise.resolve & Promise.reject
Promise.resolve可以直接把一个对象转化为Promise对象。
传入不同的对象,Promise.resolve有不同的行为
传入
Promise实例
如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。参数是一个
thenable对象thenable对象指的是具有then方法的对象,比如下面这个对象。
let thenable = {
then: (resolve, reject) => {
resolve(42);
}
};
Promise.resolve()方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then()方法。
let thenable = {
then: (resolve, reject) => {
resolve(42);
}
};
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成fulfilled了
p1.then(val => console.log(val)); // 42
let thenable = {
then: (resolve, reject) => {
reject(42);
}
};
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成rejected了
p1.catch(err => console.log(err));
let thenable = {
then: (resolve, reject) => {
throw new Error('出错了')
}
};
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成rejected了
p1.catch(err => console.log(err)); // Error: 出错了
- 参数不是具有
then()方法的对象,或根本就不是对象
let p1 = Promise.resolve('hello world');
p1.then(res => console.log(res)); // hello world
以上代码等价于
let p1 = new Promise(resolve => resolve('hello world'));
p1.then(res => console.log(res));
- 不带有任何参数
let p = Promise.resolve();
p.then(res => console.log(res)); // undefined
Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。
let p = Promise.reject('出错了');
p.catch(err => console.log(err)); // 出错了
以上代码等同于
let p = new Promise((resolve, reject) => reject('出错了'));
p.catch(err => console.log(err)); // 出错了
最后提一下,在社区有很多库实现了Promise,都遵循Primise/A+规范,有人测试社区的多种实现中,bluebird.js的性能比官方的ES6要高三倍,而且很多还在草案中的特性,bluebird.js都已经实现了,如果想要使用或者学习新的Promise特性,不妨试试bluebird.js。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com