Promise基础与实例方法
大家好,我是角落,第一次在掘金写文章,如有错误之处欢迎评论区指出。 最近在看“红宝书”,想用写文章的方式加深自己的理解。
一、以往的异步编程方式
1.定义一个异步函数
使用setTimeout模拟一个1s后才有结果的异步操作。在实际应用中,我们的异步操作可以是读取文件,发起http请求等,这里使用异步日志输出的方式 演示执行顺序和其他异步行为。
setTimeout(console.log , 0 , ...params)
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value * 2), 1000)
}
double(3) //约1000ms后得到结果6
2.通过回调函数的参数获取异步操作返回值
我们希望在异步操作之后得到返回结果,以便之后对返回结果进行操作。以往就是通过回调函数传参来实现的,见以下代码
function double(value, callback) {
setTimeout(() => {
callback(value * 2) //异步操作完成,传参并调用函数callback ,即将它推入消息队列,由运行时负责异步调度执行
}, 1000)
}
如上面代码,我们在异步操作完成之后,调用callback函数并传入返回值,那么在callback函数里就可以对返回结果进行处理,见下面代码
double(3, (res) => {
console.log(`拿到了异步操作的返回值:${res}`)
})
//结果:
//拿到了异步操作的返回值:6
3.添加失败回调
现在我们可以添加失败回调完善异步函数,当异步操作失败时,我们希望能过拿到程序返回的错误信息,方法和成功的回调一样。见下面代码:
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
// 抛出错误信息,可以被catch捕捉到
throw '传入的第一个参数必须是一个数值'
}
// 调用success函数并传入异步操作的返回结果
success(value * 2)
} catch (err) {
failure(err)
}
}, 1000)
}
可以看到,当异步操作失败时,我们调用failure函数并传参,成功时则调用success函数并传参,这些参数就是对应的执行结果和失败信息。
4.回调地狱问题
这种传统的异步解决方案会带来回调地狱的问题,即当我们需要拿到连续2个或以上的异步操作的返回结果后,再对返回结果进行操作,那么就需要嵌套。见下面代码:
//这里我们调用了一次异步double操作之后再对返回进行一次double操作
double(
3,
(res1) => {
double(res1, (res2) => {
console.log(`拿到了最终结果${res2}`)
})
},
(err) => {
console.log(err)
},
)
//输出结果
//拿到了最终结果12
如果我们的异步操作很多,那么我们就需要层层嵌套,这显然不利于代码阅读和维护,也不具有扩展性。
于是,就有了我们接下来要讲的2012年制定的Promise/A+规范
二、Promise基础
1.Promise是什么?
也许随着之后的学习我会有不同的理解,但这里说说目前为止我个人的理解。
在前面我们讲到异步操作,它一旦被执行,那么我们就必须等到它执行完毕才能通过回调函数拿到它的返回结果或执行结果。但是,这样不利于我们对于程序的掌控,如果我们可以在这个异步操作一开始执行的时候就拿到它的结果(还不存在)或者说至少得到它的状态(这里拿到状态的说法可能不太严谨,确切地说是在异步操作内部拿到,因为Promise的状态是私有的,外部无法访问和修改),那么我们可以更方便地进行后续的操作,利用Promise就可以实现这个想法。
Promise就是尚不存在的结果的一个替身,它是一个引用类型或者说对象。
它是对异步操作的一种封装,可以让我们在异步操作外部指定成功和失败的回调函数,从而避免了回调地狱的问题。
2.Promise对象的三种状态
(1) pending
待定,是Promise最初始的状态,表示尚未开始或正在执行中。
(2) fulfilled或resolved
兑现或解决,表示异步操作已经成功完成
(3) rejected
拒绝,表示异步操作失败。
这里需要特别注意的是,**Promise故意将异步行为封装起来,隔离外部同步代码。**也就是说,我们不能在外部通过Javascript检测或修改状态。
状态改变(settled)是不可逆的,且不能再改变。
3.创建Promise实例并通过执行器函数修改状态
前面说过了,Promise的状态是私有的,只能在内部对其进行操作。
我们可以通过 new Promise()来创建一个Promise实例对象,在创建Promise实例对象的时候必须传入一个函数,该函数称为执行器函数(executor),主要有两个作用:初始化异步行为和控制状态的最终转换
初始化异步行为很好理解,我们可以直接将我们的异步操作写在执行器函数里。
那么我们如何控制状态的最终转换呢?
为了控制状态的最终转换,执行器函数里提供了两个参数,它们是两个函数,一般命名为resolve和reject,下面代码是一个示例:
let p1 = new Promise((resolve, reject) => {
let value = 200
resolve(value)
})
setTimeout(console.log, 0, p1) //Promise { 200 }
let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log, 0, p2) //Promise { <rejected> undefined } 同时会报错
调用resolve函数后,Promise的状态由pending变为resolved,调用resolve函数后,Promise的状态由pending变为rejected,在调用的同时可以传入参数,这个参数一般是异步操作的结果(返回值)或错误提示信息,之后这个参数可以通过Promise的then等方法获得,如果不传入这个参数,则默认为undefined,这里后面再讲。
前面说到**状态改变(settled)是不可逆的,且不能再改变。**因此,如果我们调用resolve后又调用reject将不会产生任何效果。
由于执行器函数是Promise的初始化程序,因此它是与外部同步代码同步执行的。
看以下代码:
console.log(1)
let p = new Promise((resolve, reject) => {
// 下面这一句与外部语句同步进行,因为执行器函数是Promise的初始化程序
console.log(2)
// 模拟异步操作,1000ms后让状态变为resolved
setTimeout(resolve, 1000)
})
console.log(3)
setTimeout(console.log, 0, p) //Promise { <pending> }
setTimeout(console.log, 2000, p)
/*执行结果
1
2
3
Promise { <pending> }
Promise { undefined }
*/
4. Promise.resolve()和Promise.reject()创建Promise实例
之前我们创建状态分别为resolved和rejected的Promise实例可以这样做:
let p1 = new Promise((resolve, reject) => {
resolve(200)
})
let p2 = new Promise((resolve, reject) => {
reject('错误信息')
})
setTimeout(console.log, 0, p1)
setTimeout(console.log, 0, p2)
/* 执行结果
Promise { 200 }
Promise { <rejected> '错误信息' } 由于没有对错误进行处理因此还会输出警告
*/
现在还可以这样做
//下面代码与上面等价
let p3 = Promise.resolve(200)
let p4 = Promise.reject('错误信息')
setTimeout(console.log, 0, p3)
setTimeout(console.log, 0, p4)
/* 执行结果
Promise { 200 }
Promise { <rejected> '错误信息' }
*/
三、Promise的实例方法
1.Promise.then()
Promise.then方法用来指定处理程序,它最多接收两个参数,第一个参数是成功的回调函数,第二个参数是失败的回调函数,这里为了方便讲解给它们七个名字onResolved和onRejected它们可以在执行器函数里通过resolve()和reject()被调用。
Promise.prototype.then()返回一个新的Promise实例,该实例基于onResolved处理程序的返回值构建,其返回值会被Promise.resolve包装。
对于onResolve方法返回的Promise对象主要有以下几种情况:
let p1 = new Promise((resolve, reject) => resolve(3))
setTimeout(console.log, 0, p1) //Promise { 3 }
/* onResolved处理程序 */
// 1.不传处理程序时,原Promise实例原样向后传
let p2 = p1.then()
setTimeout(console.log, 0, p2) //Promise { 3 }
// 2.有显式的返回值,则Promise.resolve()会包装这个值
let p3 = p1.then(() => '显式的返回值')
let p4 = p1.then(() => Promise.resolve('显式的返回值'))
setTimeout(console.log, 0, p3) //Promise { '显式的返回值' }
setTimeout(console.log, 0, p4) //Promise { '显式的返回值' }
// 3.抛出异常会返回 rejected 的 Promise
let p5 = p1.then(() => {
throw '被抛出的异常'
})
setTimeout(console.log, 0, p5) //Promise { <rejected> '被抛出的异常' }
//4. 返回错误信息不会rejected,而会被包装在一个resolved的Promise里返回
let p6 = p1.then(() => new Error('错误信息'))
setTimeout(console.log, 0, p6)
/* Promise {
Error: 错误信息
at d:\CS\WEB\javascript code\第十一章-期约与异步函数\06-Promise.prototype.then().js:24:24
} */
对于onRejected方法返回的Promise对象和上面类似,这里就不赘述。
2.Promise.catch()
Promise.prototype.catch是Promise.then(null , onRejected)的语法糖
它也返回一个新的Promise实例,返回Promise实例的行为与onRejected处理程序相同。
3.Promise.finally()
Promise.prototype.finally用于给Promise添加onFinally处理程序,无论Promise转换到resolved还是rejected都会执行onFinally,该方法无法直到Promise的状态,它是一个状态无关的方法,大部分时候都是返回原父Promise
返回新Promise的情况见以下代码
// 1.以下情况都返回原父Promise 即原样后传
let p2 = p1.finally()
let p3 = p1.finally(() => {})
let p4 = p1.finally(() => Promise.resolve('bar'))
let p5 = p1.finally(() => 'bar')
setTimeout(console.log, 0, p2) //Promise { 'foo' }
setTimeout(console.log, 0, p3) //Promise { 'foo' }
setTimeout(console.log, 0, p4) //Promise { 'foo' }
setTimeout(console.log, 0, p5) //Promise { 'foo' }
// 2. 当onFinally处理程序 返回一个pending Promise 或 抛出错误 时,返回相应Promise
let p6 = p1.finally(() => new Promise(() => {}))
let p7 = p1.finally(() => {
throw '抛出的错误'
})
setTimeout(console.log, 0, p6) //Promise { <pending> }
setTimeout(console.log, 0, p7) //Promise { <rejected> '抛出的错误' }
4.Promise方法的非重入特性
前面提到的then、catch 、finally都是非重入Promise方法。
当Promise settled时,处理程序仅仅会被排期而不是立即执行,处理程序会被推进消息队列。
只有等当前线程的同步代码执行完,才会执行处理程序。
如果有多个处理程序,则按添加顺序执行。
如以下代码:
let p1 = Promise.resolve()
setTimeout(console.log, 0, p1)
p1.then(() => {
console.log('第一个onResolved处理程序')
})
p1.then(() => {
console.log('第二个onResolved处理程序')
})
p1.then(() => {
console.log('第三个onResolved处理程序')
})
console.log('then方法后的同步代码')
/* 结果
then方法后的同步代码
第一个onResolved处理程序
第二个onResolved处理程序
第三个onResolved处理程序
Promise { undefined }
*/
四、用Promise封装一个异步读取文件的方法
首先,我们想封装一个读取文件的函数,命名为 getFile(),这个函数的返回值应该是一个Promise实例对象,这样我们才可以调用前面讲的那些实例方法,于是代码如下:
function getFile() {
// 返回一个Promise实例,这样调用这个函数之后就可以使用then等方法处理文件
return new Promise()
}
接下来我们必须往Promise()里传入一个执行器函数来定义我们读取文件的操作,读取文件我们使用fs模块的readFile方法,因此需要先导入,读取文件需要传入一个文件路径fPath,代码如下:
const fs = require('fs')
function getFile(fPath) {
// 返回一个Promise实例,这样调用这个函数之后就可以使用then方法处理文件
return new Promise(() => {
fs.readFile(fPath, 'utf8', (err, dataStr) => {})
})
}
在readFile方法的回调函数里,我们需要对读取结果进行操作,还需要把读取结果向外部传,因此需要用到resolve和reject函数。
const fs = require('fs')
function getFile(fPath) {
// 返回一个Promise实例,这样调用这个函数之后就可以使用then方法处理文件
return new Promise((resolve, reject) => {
fs.readFile(fPath, 'utf8', (err, dataStr) => {
if (err) return reject(err) //读取失败,调用'失败'的回调函数
resolve(dataStr) //读取成功,调用'成功'的回调函数
})
})
}
ok,现在我们已经封装完成,可以用它来进行异步读取文件的操作了。
getFile('../files/file1.txt')
.then((str) => { //成功的回调函数
console.log(str)
})
.catch((err) => {
console.log(err.message)
})