1、Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise
对象特点
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:Pending
(进行中)、Resolved
(已完成,又称 Fulfilled)和Rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从Pending
变为Resolved
和从Pending
变为Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
缺点
- 无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 - 当处于
Pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise对象主要功能是解决异步的回调问题
2、同步和异步
当cpu面对一个长时间、其他设备处理,比如磁盘I/O,数据库查询、写入等等这些操作的时候,此时有两种模式:第一种是同步模式,死等这个设备处理完成,此时cpu被堵塞了;第二种就是异步模式,不等这个设备处理完成,而是先执行后面的语句,等这个设备处理完成之后执行回调函数。
异步的小案例
先让cpu处理累加计算的逻辑,然后再命令硬盘异步读取文件,此时读取过程中没有堵塞进程,cpu提前执行后面的累加程序,等读取完毕之后再执行读取内部的回调函数
var fs = require("fs");
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
// 读取文件
fs.readFile("./txt/01.txt",(err,content)=>{
console.log(content.toString())
})
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
同步小案例
var fs = require("fs");
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
// 同步读取文件
var content = fs.readFileSync("./txt/01.txt")
console.log(content.toString())
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
同步和异步的区别从语法上看,同步是在等号左侧接收,异步是在回调函数内部接收结果
除了处理语法层面上不同之外,时间上也有区别,同步代码的时间,会比异步代码的耗费时间要长
3、异步的语法缺点
3.1 代码不美观
fs.readFile("./txt/01.txt",(err,content)=>{
console.log(content.toString())
fs.readFile("./txt/02.txt",(err,content)=>{
console.log(content.toString());
fs.readFile("./txt/03.txt",(err,content)=>{
console.log(content.toString());
})
})
})
同步的话看起来美观多了
console.log(fs.readFileSync("./txt/01.txt"));
console.log(fs.readFileSync("./txt/02.txt"));
console.log(fs.readFileSync("./txt/03.txt"));
3.2 代码的维护性和机构性不强
promise来解决这个问题
4、Promise的基本使用
var fs = require("fs")
function rFile(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,(err,content)=>{
if(err) {
reject(err)
return;
}
resolve(content.toString())
})
})
}
rFile("./txt/01.txt").then((data)=>{
console.log(data);
return rFile("./txt/02.txt")
})
.then(data=>{
console.log(data);
return rFile("./txt/03.txt")
})
.then(data=>{
console.log(data);
})
- rFile函数返回了一个Promise的对象,这个对象就是构造函数,是ES6新增的
- Promise一定是某一个函数的返回值,Promise必须当做一个函数的返回值才有意义,上面代码中Promise就是rFile函数的返回值
- Promise在被new的时候要求传入一个函数,这个函数就是Promise的回调函数,这个函数有两个形参,分别是resolve和reject;resolve表示成功之后做的事情,reject表示失败之后做的事情
then和catch
下面代码中then的部分表示的就是读取成功之后的操作,catch表示的是读取失败的操作
rFile("./txt/011.txt")
.then((data)=>{
console.log(data)
}).catch(err=>{
console.log("读取错误",err);
})
下面的catch一旦遇到了错误就会阻止整个程序的执行,比如读取两个文件,第一个文件读取失败了
rFile("./txt/011.txt").then((data)=>{
console.log(data);
return rFile("./txt/02.txt")
}).then((data)=>{
console.log(123)
}).catch(err=>{
console.log("读取错误",err);
})
上面代码中,由于没有011.txt,所以会走catch,此时你会发现第二个then中123也没有输出
如果想要输出第二个then中的data可以使用单个then报错的方式,第二个参数
rFile("./txt/011.txt").then((data)=>{
console.log(data);
return rFile("./txt/02.txt")
},(err)=>{
console.log("读取错误1",err);
}).then((data)=>{
console.log(123)
})
上的代码中虽然011.txt没有抛出错误了,但是没有阻止下一个then的执行,输出了123
总结:
- Promise函数中,内部的语句一定是异步语句,异步语句的成功将通过resolve(成功的数据)传出去,这个数据将成为后面调用的then函数中的data返回值,异步语句的失败将通过reject(失败的数据)传出去,这个数据将成为后面调用的catch函数中的err返回值,失败的语句也可以是then的第二个参数
- Promise的实例拥有then的能力,then里面接收一个参数,data就是创建promise的时候的resolve;then进行连续打点
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
rFile("./txt/01.txt").then((data)=>{
console.log(data);
return rFile("./txt/02.txt")
}).then((data)=>{
console.log(data)
return rFile("./txt/03.txt")
}).then(data=>{
console.log(data)
})
5、非Node环境使用Promise
5.1浏览器直接调用 ajax读取文件
<script>
$("h1").click(function () {
$.get("txt/01.txt", function (data) {
console.log(data);
$.get("txt/02.txt", function (data) {
console.log(data);
$.get("txt/03.txt", function (data) {
console.log(data)
})
})
})
})
</script>
缺点:回调黑洞问题
5.2 浏览器使用 Promise 解决回调黑洞
function readFile(url) {
return new Promise((resolve, reject) => {
$.get(url, (data) => {
resolve(data);
})
})
}
$("h1").click(function () {
readFile("./txt/01.txt").then((data) => {
console.log(data);
return readFile("./txt/02.txt")
}).then(data => {
console.log(data);
return readFile("./txt/03.txt")
}).then(data => {
console.log(data)
})
})
Promise本质上就是一个“语法糖”
语法糖?
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会
所以Promise其实就是改变了异步的语法,本质没有改变异步,而是将异步嵌套改为then的连续打点写法
6、Promise.resolve方法
Promise可以.then调用是因为当前的实例是一个promise对象,promise对象是怎么形成的?
可以用promise.resolve方法去创建一个对象
var pr = Promise.resolve("hello");
console.log(pr)
node 07.js
上面的代码将"hello"封装成了一个带有promise对象的壳子
此时这个pr就可以.then去使用
var pr = Promise.resolve("hello");
pr.then(data=>{
console.log(data)
})
如果resolve方法的参数是一个对象内部有then方法
Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法
let thenable ={
then : function (resolve,reject) {
resolve('hello')
}
}
let p1 = Promise.resolve(thenable)
p1.then(function (value) {
console.log(value)
})
7、Promise.all方法
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例
var fs = require("fs");
function readFile(url) {
return new Promise((resolve,reject) => {
fs.readFile(url,(err,content) =>{
if(err){
reject(err);
return;
}
resolve(content.toString())
})
})
}
var p1 = readFile("project/txt/01.txt")
var p2 = readFile("project/txt/02.txt")
var p3 = readFile("project/txt/03.txt")
Promise.all([p1,p2,p3]).then(data=>{
console.log(data)
}).catch(err => {
console.log(err);
})
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例,如果不是,就会先调用到Promise.resolve方法,将参数转为Promise实例,再进一步处理
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成成功状态,p的状态才会变成成功,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个为失败,p的状态就变成失败,此时第一个被reject的实例的返回值,会传递给promise.all的catch回调函数
8、Promise.race方法
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例
var fs = require("fs")
function readFile(url) {
return new Promise((resolve,reject) =>{
fs.readFile(url,(err,content) =>{
if(err){
reject(err);
return;
}
resolve(content.toString())
})
})
}
var p1 = readFile("project/txt/01.txt")
var p2 = readFile("project/txt/02.txt")
var p3 = readFile("project/txt/03.txt")
var p = Promise.race([p1,p2,p3])
p.then(data =>{
console.log(data)
}).catch(err=>{
console.log(err)
})
和promise.all方法不同的是只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
9、Promise.done方法-非ES6提供
Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
var fs = require("fs");
Promise.prototype.done = function(onFulfilled, onRejected){
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { console.error("信息错误") }, 0);
});
}
function readFile(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,(err,content)=>{
if(err){
reject(err)
return;
}
resolve(content.toString())
})
})
}
var p1 = readFile("txt/011.txt")
var p = Promise.race([p1])
p.then(data=>{
console.log(data);
}).done()
上面的011.txt是没有的所以抛出全局错误
10、Promise.finally方法-非ES6提供
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
var fs = require("fs");
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 })
);
};
function readFile(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,(err,content)=>{
if(err){
reject(err)
return;
}
resolve(content.toString())
})
})
}
var p1 = readFile("txt/01.txt")
var p = Promise.race([p1])
p.then(data=>{
console.log(data);
}).finally(()=>{
console.log("最后的执行");
})