Promise对象
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise
对象有以下两个特点。
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:Pending
(进行中)、Resolved
(已完成,又称 Fulfilled)和Rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从Pending
变为Resolved
和从Pending
变为Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于Pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 stream 模式是比部署Promise
更好的选择。
我们看一下异步和同步
当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("./text.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("./text.txt") console.log(content.toString()) // 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()) fs.readFile("./txt/02.txt",(err,content)=>{ console.log(content.toString()); fs.readFile("./txt/03.txt",(err,content)=>{ console.log(content.toString()); }) }) })
此时如果我们改成同步的,的确就美观了,此时我们也会发现cpu的效率变低了
console.log(fs.readFileSync("./txt/01.txt")); console.log(fs.readFileSync("./txt/02.txt")); console.log(fs.readFileSync("./txt/03.txt"));
第二个缺点是异步的语法造成了,代码的维护性和机构性不强
Promise的基本使用
var fs = require("fs") function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err) { reject(err) return; } resolve(content.toString()) }) }) } //处理我们的回调逻辑的部分 File("./text.txt").then((data)=>{ console.log(data); return File("./text2.txt") }) .then(data=>{ console.log(data); return File("./text3.txt") }) .then(data=>{ console.log(data); })
Promise的封装
function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err) { reject(err) return; } resolve(content.toString()) }) }) }
- File函数返回了一个Promise的对象,这个对象就是构造函数,是ES6新增的
- Promise一定是某一个函数的返回值,Promise必须当做一个函数的返回值才有意义,上面代码中Promise就是File函数的返回值
- Promise在被new的时候要求传入一个函数,这个函数就是Promise的回调函数,这个函数有两个形参,分别是resolve和reject;resolve表示成功之后做的事情,reject表示失败之后做的事情
下面代码中then的部分表示的就是读取成功之后的操作,catch表示的是读取失败的操作
File("./text.txt") .then((data)=>{ console.log(data) }).catch(err=>{ console.log("读取错误",err); })
下面的catch一旦遇到了错误就会阻止整个程序的执行,比如读取两个文件,第一个文件读取失败了
File("./text1.txt").then((data)=>{ console.log(data); return rFile("./text.txt") }).then((data)=>{ console.log(123) }).catch(err=>{ console.log("读取错误",err); })
上面代码中,发现由于没有text1.txt,所以会走catch,此时你会发现第二个then中123也没有输出
如果想要输出第二个then中的data可以使用单个then报错的方式,第二个参数
File("./text1.txt").then((data)=>{ console.log(data); return rFile("./text2.txt") },(err)=>{ console.log("读取错误text1",err); }).then((data)=>{ console.log(data) })
上面的代码中虽然text1.txt没有抛出错误了,但是没有阻止下一个then的执行,输出了then中的data为underfined。
注意:
- Promise函数中,内部的语句一定是异步语句,异步语句的成功将通过resolve(成功的数据)传出去,这个数据将成为后面调用的then函数中的data返回值,异步语句的失败将通过reject(失败的数据)传出去,这个数据将成为后面调用的catch函数中的err返回值,失败的语句也可以是then的第二个参数
- Promise的实例拥有then的能力,then里面接收一个参数,data就是创建promise的时候的resolve;then进行连续打点,then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
File("./text.txt").then((data)=>{ console.log(data); return File("./text2.txt") }).then((data)=>{ console.log(data) return File("./text3.txt") }).then(data=>{ console.log(data) })
非Node环境使用Promise
我们看一下浏览器调用Ajax读取文件
<script> $("#button").click(function(){ $.get("./text.txt",function(data){ console.log(data); $.get("./text2.txt",function(data){ console.log(data); $.get("./text3.txt",function(data){ console.log(data); }) }) }) console.log("输出了"); }) </script>
发现请求也是异步的,并且也有回调黑洞的问题
可以使用Promise去解决这个问题
function readFile(url){ return new Promise((resolve,reject)=>{ $.get(url,(data)=>{ resolve(data) }) }) } $("#button").click(function(){ readFile("./text.txt").then(data=>{ console.log(data); return readFile("./text2.txt") }).then(data=>{ console.log(data); return readFile("./text3.txt") }).then(data=>{ console.log(data); }).catch(err=>{ console.log("读取错误",err); }) console.log("输出了") })
Promise本质上就是一个“语法糖”
什么是语法糖?语法糖(Syntactic sugar),也译为糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会
所以Promise其实就是改变了异步的语法,本质没有改变异步,而是将异步嵌套改为then的连续打点写法
Promise.resolve方法
Promise可以.then调用是因为当前的实例是一个promise对象,promise对象是怎么形成的?我们可以用promise.resolve方法去创建一个对象
var str = Promise.resolve("hello"); console.log(str);
发现上面的代码将"hello"封装成了一个带有promise对象的壳子
此时这个str就可以.then去使用
var str = Promise.resolve("hello"); str.then(data=>{ console.log(data) })
如果resolve方法的参数是一个对象内部有then方法
Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行str2对象的then方法
let str2 = { then: function(resolve, reject) { resolve("hello"); } }; let str = Promise.resolve(str2); str.then(function(value) { console.log(value); });
Promise.all方法
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例
var fs = require("fs"); function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err){ reject(err) return; } resolve(content.toString()) }) }) } var str1 = File("./text.txt") var str2 = File("./text2.txt") var str3 = File("./text3.txt") Promise.all([str1,str2,str3]).then((data)=>{ console.log(data); }).catch(err=>{ console.error(err); })
上面代码中,Promise.all方法接受一个数组作为参数,str1、str2、str3都是Promise对象的实例,如果不是,就会先调用到Promise.resolve方法,将参数转为Promise实例,再进一步处理
str的状态由str1、str2、str3决定,分成两种情况。
(1)只有str1、str2、str3的状态都变成成功状态,str的状态才会变成成功,此时str1、str2、str3的返回值组成一个数组,传递给str的回调函数。
(2)只要str1、str2、str3之中有一个为失败,str的状态就变成失败,此时第一个被reject的实例的返回值,会传递给promise.all的catch回调函数
Promise.race方法
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例
var fs = require("fs"); function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err){ reject(err) return; } resolve(content.toString()) }) }) } var str1 = File("./text.txt") var str2 = File("./text2.txt") var str3 = File("./text3.txt") var strAll=Promise.race([str1,str2,str3]) strAll.then(data=>{ console.log(data); }).catch(err=>{ return err })
和promise.all方法不同的是只要str1、str2、str3之中有一个实例率先改变状态,strAll的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给strAll的回调函数;
两个非es6提供的方法
Promise.done方法
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 File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err){ reject(err) return; } resolve(content.toString()) }) }) } var str = File("./tex1t.txt") var str2 = Promise.race([str]) str2.then(data=>{ console.log(data); }).done()
Promise.finally方法
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
读正确的文件:
var fs = require("fs"); Promise.prototype.finally = function (callback) { let S = this.constructor; return this.then( value => S.resolve(callback()).then(() => value), reason => S.resolve(callback()).then(() => { throw reason }) ); }; function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err){ reject(err) return; } resolve(content.toString()) }) }) } var str = File("./text.txt") var str2= Promise.race([str]) str2.then(data=>{ console.log(data); }).finally(()=>{ console.log("最后执行"); })
读错误的文件:
var fs = require("fs"); Promise.prototype.finally = function (callback) { let S = this.constructor; return this.then( value => S.resolve(callback()).then(() => value), reason => S.resolve(callback()).then(() => { throw reason }) ); }; function File(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,(err,content)=>{ if(err){ reject(err) return; } resolve(content.toString()) }) }) } var str = File("./te1xt.txt") var str2= Promise.race([str]) str2.then(data=>{ console.log(data); }).finally(()=>{ console.log("最后执行"); })
此时会报错,但依旧会执行回调函数