文章目录
Promise
写在前面
本博文仅作为个人学习过程的记录,可能存在诸多错误,希望各位看官不吝赐教,支持错误所在,帮助小白成长!
一、认识Promise
Promise是什么?!
Promise是ES6提出的异步编程的一种新规范。
旧版本中完成异步任务都是通过简单的回调任务!
Promise解决什么问题?
由于JavaScript作为脚本语言,解释执行的特点所有的代码执行都是单线程,就导致有些操作需要借助异步操作来完成。而异步的任务的执行顺序难以控制。
例如下面的代码:(JavaScript中使用Node的内建模块fs操作文件就是异步任务):
const fs = require('fs')
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
fs.readFile('./2.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
fs.readFile('./3.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
fs.readFile('./4.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
代码并不会按照你所写代码的顺序执行,而是通过异步回调的方式执行,所以你就可能看到这样的结果:
![image-20210524151708057](https://i-blog.csdnimg.cn/blog_migrate/331d9c5559cd23c21a24545c2ddbee57.png)
如果我们想要读取的顺序受我们控制,我们就只能在回调函数中嵌套另一个异步任务,就像这样:
const fs = require('fs')
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
fs.readFile('./2.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
fs.readFile('./3.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
fs.readFile('./4.txt', 'utf-8', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
})
})
})
这样写的代码,输出的顺序永远是1,2,3,4,因为只有上一个文件的内容输出完成后才会启动下一个异步任务!
但是这个代码…,是不是看的头疼?!这就是**回调地狱!(callback hell)**有两个明显的缺陷:
- 难以阅读!
- 容易出错,且难以排查!
Promise的优势
- 允许链式调用,解决回调地狱问题
- 逻辑清晰,便于理解
二、Promise初上手
2.1、基本案例
我们先用一个最简单的例子,来演示一下Promise的使用:
就以上面读取文件为例:
首先我们创建一个Promise对象:
![image-20210524154004453](https://i-blog.csdnimg.cn/blog_migrate/206486ca0e85d590662a07f0a47d5cb8.png)
哇擦,这个参数你一看,头皮发麻。
Promise的构造函数,需要一个函数executer
(也即你的异步任务),而这个异步任务函数的参数是所两个函数,函数名分别为resolve
,reject
(这是我推荐的一种命名规范!)
resolve函数:函数类型是(value) => void
,先说一下,这个函数可以在你异步任务中调用,会将你的异步任务执行状态从待定(pending)转换为(fulfilled)成功!同时这个函数可以传入你异步任务的执行结果作为函数参数value
!
reject函数:函数类型的(reason) => void
,与resolve函数相反,它会将你的异步任务执行状态从待定(pending)转为(rejected)失败!并且可以传入一个值作为你异步任务的出错的原因,然后在具体的reject函数实现中进行处理!
const fs = require('fs')
const promise = new Promise((resolve, reject) => {
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if (err) {
// 调用reject, 标识任务失败,同时传入err作为失败的reason
reject(err)
} else {
// 调用resolve, 标识任务成功,传入data作为异步任务的执行结果
resolve(data)
}
})
})
现在我们要知道几点:
- Promise的状态装换有两种:
pending -> fulfilled
与pending -> rejected
。 resolve
、reject
是两个回调函数,分别会在调用时将你的异步任务状态修改为fulfilled、rejected。我们在异步任务中调用了它们,但是并没有为它们写具体的实现
目前我们的代码运行,没有任何动静
现在通过promise对象的then
方法,对两种任务状态下的回调函数进行实现:
then
方法的两个参数分别刚好与resolve、reject函数的类型相同:
![image-20210524162118938](https://i-blog.csdnimg.cn/blog_migrate/82e5f589d46afb48ab43d305e443cc3a.png)
(并且then方法的返回值还是一个Promise对象,这是方便链式调用的独特设计!)
promise.then(
// 任务转态为成功时,执行此函数, value为异步任务中调用resolve传入的执行结果
(value) => {
console.log(value)
},
// 任务状态为失败时,执行此函数,reason为你在异步任务中传入的失败原因reason
(reason) => {
console.log(reason)
},
)
然后就可以开开心心测试代码啦~~
2.2、让小屁孩都能懂的解释
看完代码,不禁我都有疑问,为什么可以这样写?!这是个什么思路?!对于Java代码写的多的我,无法理解为什么!!
直到我看到了这篇文章:我以为我很懂Promise,直到我开始实现Promise/A+规范 - 知乎 (zhihu.com)
那么我们就从这项技术的命名开始说起,毕竟它的名字就能代表它的绝大部分思想的源泉!Promise,直译就是承诺!我们简称它为画饼。
那么我们创建Promise实例的过程也就是我们画饼的过程,这个具体的饼也就是Promise创建时我们传入的异步任务!
我们画饼时就会强调后果:
- 如果画出来了,各位就工资几何翻倍,别墅靠海。这也就是我们在“饼”中写下的
resolve()
- 如果饼没了,各位就天天加班,月月低保。这就是“饼”中的
reject()
当然这都是在画饼,都还是虚构的,还没有真正实现,谁都不清楚“饼”的结果如何!即这个饼的状态还是pending状态,它有可能fulfilled,也有可能变成rejected!(这取决于这个“饼”内部的走向)
既然画了饼,那必然有人来接这个”饼“,如果饼实现了我就朝九晚五,房车不愁。如果饼没了打工仔我整日就以泪洗面。而这个接饼的过程就是我们常见的发布与订阅模式,而在Promise的概念中,订阅就是通过then
方法来实现,所有的订阅者需要做好两方面的心理准备!这两手心理准备的实现就是在then方法的两个参数函数中体现。
除了一个饼可以多个人同时订阅外,一个饼还可以一层层往下传,也就是后面会学习的链式调用!比如老板先画了一个大饼给经理,然后经理又给部门组长画了一个饼,然后组长又给组员画了一个饼。这就是promise.then(..).then(..).then(..)
的结果!
2.3、util.promisify
如果所有的异步任务,都需要我们进行手动包装为Promise版本,写起来还是比较麻烦的。于是Node中util模块中提供了一个函数promisify
:传入一个遵循常见的错误优先的回调风格的函数(即以 (err, value) => ...
回调作为最后一个参数),并返回一个返回 promise 的版本。
我们先来搞清楚什么叫遵循常见的错误优先的回调风格的函数
- 函数参数中有一个回调函数
- 回调函数是这个函数最后一个参数
- 回调函数的参数列表中错误优先,例如
(err, value) => { ... }
很显然,我们刚才使用的fs.readFile()
就满足这个条件!!
那现在我们利用这个工具,来将其进行封装:
const util = require('util')
const fs = require('fs')
// 封装返回一个Promise对象
const promiseReadFile = util.promisify(fs.readFile)
promiseReadFile('./1.txt', 'utf8').then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
},
)
三、Promise状态与对象值
在基础案例中,我们说过Promise对象的三个状态:
pending
resolved/fulfilled
rejected
我可不是胡编乱造,看代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport