Promise特点

本文最后更新于:8 个月前

为什么要有Promise、它的出现是为了解决什么问题、Promise的基本介绍

Promise特点

一、为什么要有Promise

我们在学习一个东西之前,首先得知道这个东西是用来干什么的。否则稀里糊涂学了一大堆,到最后越学越懵,别人问你一个最简单的问题Promise是用来干什么的,你憋了半天回答不出来

1.1.解决什么问题

  • (1)执行代码与结果处理逻辑分离,(说人话:“指定回调函数的方式更灵活”)
  • (2)解决“回调地狱”

第一个场景,我们通过setTimeout模拟一个异步操作,异步操作中生成一个随机数,结果有两种状态,这个数要么是奇数、要么是偶数,针对两种状态做不同的处理(对应到其它实际的异步操作中,就是要么成功、要么失败)

分别使用「回调」、「Promise」的方式,对不同的结果状态进行处理,来对比一下两者有什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 回调的方式
function generate_even_number(success, failure){ // success和failure是传入的回调函数
setTimeout(()=>{
const num = Math.ceil(Math.random()*10) // 生成从1到10的随机数
if(num%2===0) success(num) // 若num为偶数,调用success函数并通过回调函数的参数返回num
else failure() // 若num为奇数,调用failure函数通知生成偶数失败
},1000)
}

generate_even_number((result)=>{
console.log('even_number: ',result)
}, ()=>{
console.log('failure')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Promise的方式
function generate_even_number(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
const num = Math.ceil(Math.random()*10)
if(num%2===0) resolve(num)
else reject()
})
})
}

const p1 = generate_even_number() //p1是一个Promise实例
const p2 = p1.then((result)=>{console.log('even_num: ',result)})
const p3 = p2.catch(()=>{console.log('failure')})

对比一下两者,可以看到

  • 「回调」的方式必须在执行代码(调用generate_even_number)的同时指定结果的处理逻辑(传入两个回调函数)
  • 「Promise」的方式,可以先执行代码,而后再来指定对结果的处理逻辑

做到了执行代码、处理结果分离(可是这样有什么好处呢?依然没get到)



第二个场景,先后读取三个文本,按顺序进行打印

同样分别使用两种方式进行对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fs = require('fs')
fs.readFile('a.txt',(err,txt)=>{
if(err) throw new Error(err)
else{
print(txt)
fs.readFile('b.txt',(err2,txt2)=>{
if(err2) throw new Error(err2)
else{
print(txt2)
fs.readFile('c.txt',(err3,txt3)=>{
if(err3) throw new Error(err3)
print(txt3)
})
}
})
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Promise的方式
var fs = require('fs')
function readFile(url){
return new Promise(resolve, reject){
fs.readFile(url, (err,txt)=>{
if(err) reject(err)
else resolve(txt)
})
}
}
readFile('a.txt').then((result)=>{
print(result)
return readFile('b.txt')
}).then((result)=>{
print(result)
return readFile('c.txt')
}).then((result)=>{
print(result)
}).catch((err)=>{
throw new Error(err)
})

通过观察可以看到,在执行串行任务时,任务数量越多,「回调」这种方式的嵌套层数就越深,代码可读性越差,所以被称为“回调地狱”。「Promise」的优势在于可以通过不断地调用then方法,进行链式调用,代码结构清晰!


1.2.缺点

  • 无法取消Promise,一旦新建、立即执行、无法取消
  • 如果不设置reject回调,内部抛出的错误不会反应到外部

1.3 Promise是什么

Promise实际上应该是一个规范:Promises/A+ (promisesaplus.com)。只是各个浏览器厂商将这个规范实现出来供我们使用。



二、Promise构造器

2.1 构造器语法

1
2
3
new Promise(executor)
//参数executor:函数,在构造Promise实例对象过程中,被构造函数执行
//返回值:Promise实例对象



2.2 executor函数

executor的函数签名为:

1
2
3
4
function(resolve, reject){...}
//resolve和reject,函数类型,可以使用任何名字
//这两个函数可以接受任何类型的单个参数
//resolve的参数可能是另一个promise对象

如果在executor中抛出错误,该promise对象将被拒绝

executor的两项主要功能:

  • 初始化Promise的异步行为
  • 控制Promise实例状态的最终转换,通过resolve/reject转换



2.3 异步调用的流程

  1. executor内执行异步操作,提供回调函数(接口)
  2. 回调通过调用resolve终止,resolve的调用包含一个value参数
  3. value被返回到绑定的Promise对象上
  4. Promise对象调用.then(handleResolved)
  5. .then(handleResolved)收到value作为参数传递给handleResolved进行使用



2.4 Promise用法示例

正确的初始化语法:构造器函数中必须提供回调函数作为参数

1
2
3
var p = new Promise			//Uncaught TypeError: Promise resolver undefined is not a function
var p = new Promise() //Uncaught TypeError: Promise resolver undefined is not a function
var p = new Promise(()=>{}) //Promise {<pending>}

executor函数中通过resolve()和reject参数函数控制状态转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p = new Promise((resolve,reject)=>{		//1) 创建Promise对象,指定执行器executor函数
setTimeout(()=>{ //2) 在执行器函数中,启动异步任务
const time = Date.now()
if(time%2 == 1){ //3)根据结果做不同的处理
resolve("成功 "+time) //成功,调用resolve(),指定成功的value,同时Promise变为resolved状态
}else{
reject("失败 "+time) //失败,调用reject(),指定失败的reason,同时Promise变为rejected状态
}
}, 2000);
});
p.then( //指定Promise对象的回调函数,有两个参数
(value) => {console.log("成功,value:",value)}, //成功的回调函数,onResolved,得到成功返回的value
(reason) => {console.log("失败,reason:",reason)} //失败的回调函数,onRejected,得到失败的reason
);
//成功,value: 成功 1675872125511



三、Promise的3种状态

3.1 三种状态

  • pending,待定。(初始化状态)可以转换为另外两种状态之一,一经转换不能再改变
  • resolved,解决,(有时也写做 fulfilled),带有一个value值
  • rejected,拒绝,带有一个reason值
1
2
3
var p1 = new Promise(()=>{})		  //Promise {<pending>}
var p2 = Promise.resolve('解决') //Promise {<fulfilled>: '解决'}
var p3 = Promise.reject('拒绝') //Promise {<rejected>: '拒绝'}

状态转换是不可逆的。pending一旦转换为resolved或rejected之后,任何方法都无法再改变该实例的状态。



3.2 改变状态的方法

  • executor中调用resolve/reject转换
  • 调用静态方法resolve/reject直接获得非pending状态的Promise实例
1
2
3
4
5
6
7
const p = new Promise((resolve,reject)=>{
setTimeout(()=>{
const time=Date.now();
if(time%2==1){resolve("成功"+time);} //resolve()将状态从pending转为resolved
else {reject("失败"+time);} //reject()将状态从pending转为rejected
},1000);
});
1
2
3
4
let p1 = Promise.resolve()
let p2 = Promise.reject()
p1 //Promise {<fulfilled>: undefined}
p2 //Promise {<rejected>: undefined}



四、Promise方法概述

4.1.静态方法

Promise静态方法定义在构造函数里,如下

image-20230209001400764

以下只做列举,方法的详细细节请看这一篇

方法 说明
resolve 把任何值都转换为一个Promise,它只对第一个参数起作用
reject 实例化一个拒绝的Promise并抛出一个异步错误,只接受一个参数作为reason
all 等待一组Promise实例全部resolved之后,返回PromiseResult数组。如果有一个实例reject了,将被迫中止
allSettled 等待一组Promise实例全部resolved之后,返回PromiseResult数组。即使有实例reject了,照常执行完
race 返回一组Promise实例集合中最先落定的Promise实例
any 返回一组中最先resolved的Promise实例,只有所有实例都rejected了返回的才是rejected的实例



4.2.实例方法

Promise实例方法定义在Promise原型上

image-20230513114023572

以下只做列举,方法的详细细节请看这一篇

方法 说明
then 为Promise实例添加处理程序
catch 语法糖,用于给期约添加拒绝处理程序
finally 用于给Promise实例添加onFinally处理程序,不管实例是resolved还是rejected状态都会执行



五、Promise模拟实现(完整)

实现 Promise 最复杂的部分在于 then的链式调用和值穿透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class myPromise {
constructor(executor) {
this.status = 'pending' // 初始状态
this.value = undefined // 保存PromiseResult
this.reason = undefined // 保存PromiseReason
this.onResolvedCallbacks = [] // onResolved处理程序数组
this.onRejectedCallbacks = [] // onRejected处理程序数组

// 调用此方法就是成功
let resolve = (value) => {
// 如果value是一个promise,递归进行解析
if (value instanceof myPromise) {
return value.then(resolve, reject)
}

// 实例只有在还未落定的前提下,才能转换状态
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = value

// 转换状态之后,将所有收集到的onResolved处理程序执行一遍(发布-订阅模式)
this.onResolvedCallbacks.forEach(cb => cb(this.value))
}
}

let reject = (reason) => {
// 实例只有在还未落定的前提下,才能转换状态
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason

// rejected之后,将所有onRejected处理程序执行一遍
this.onRejectedCallbacks.forEach(cb => cb(this.reason))
}
}

// 构造实例的时候,执行传入的函数
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}

// 简单版,没有考虑链式调用、值透传的实现
then(onResolved, onRejected) {

onResolved = typeof onResolved === 'function' ? onResolved : v => v
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }

// 如果实例还没有落定,将处理程序先保存起来
if (this.status === 'pending') {
this.onResolvedCallbacks.push(onResolved)
this.onRejectedCallbacks.push(onRejected)
}

// 如果实例resolved,调用onResolved处理程序,并将PromiseResult作为调用参数
else if (this.status === 'fulfilled') {
onResolved(this.value)
}

// 如果实例rejected,调用onRejected处理程序,并将PromiseReason作为调用参数
else {
onRejected(this.reason)
}
}

// catch实际上是语法糖
catch(onRejected) {
return this.then(null, onRejected)
}

// 静态类方法,默认生成一个resolved的Promise实例。注意:与constructor中的resolve并不是一回事,虽然它们名字相同
// Promise.resolve是具备等待功能的,所以在constructor的resolve需要进一步完善
static resolve(data) {
return new myPromise((resolve, reject) => {
resolve(data)
})
}

static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}

static all(promises) {
// 如果传入的参数不是数组,不执行
if (!Array.isArray(promises)) {
throw new TypeError('Promises must be an array!');
}

// all方法返回一个Promise实例,PromiseResult为参数中所有Promise的result按顺序组成的数组
return new Promise((resolve, reject) => {
let resultArr = [] // 用来保存所有Promise实例的PromiseResult
let count = 0 // 用来记录resolved的实例数量
const processResultByKey = (value, i) => {
result[i] = value
if (++count === promises.length) resolve(resultArr)
}
for (let i = 0; i < promises.length; i++) {
let promise = promises[i]
promise.then(data => {
processResultByKey(data, i)
}, reason => {
reject(reason)
})
}
if (promises.lenth === 0) {
resolve([])
}
})
}

static race(promises) {
if (!Array.isArray(promises)) {
throw new TypeError('Promises must be an array!');
}

// race方法返回一个实例,结果为第一步resolved或rejected的实例
return new myPromise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let premise = promises[i]
promise.then(data => {
resolve(data)
}, reason => {
reject(reason)
})
}
})
}
}

参考链接

Promise - 廖雪峰的官方网站 (liaoxuefeng.com)

面试官:“你能手写一个 Promise 吗” - 知乎 (zhihu.com)


Promise特点
http://timegogo.top/2023/05/13/JavaScript/JavaScript异步(二)Promise特点/
作者
丘智聪
发布于
2023年5月13日
更新于
2023年7月16日
许可协议