异步回调与Promise

在 ajax 中,我们提到了 Promise 式封装

那么什么是 Promise 呢?

首先要来介绍一下什么是异步回调

我们在计算机网络中学过通信方式一般有三种:全双工、半双工、单工

最理想的方式当然是全双工

异步回调

异步

异步也是一种全双工

简单来说,就是当一个任务发出后,发件人不需要等待执行方的返回结果,而可以先去做其它事情,等待

比如你加某个人为 qq 好友,发出请求后你就不需要管了,可以自己先去聊天,等到对方同意或拒绝你的好友请求,你才会收到通知,回来处理这件事

回调

那回调又是什么呢?回调其实解决了异步的一个非常重要的问题——我要这个结果做什么用,简单说就是当异步结果返回的时候,该做什么呢

在上文 qq 好友的例子中,qq 通知你好友请求返回结果了,你根据结果做出的动作就是回调

比如好友请求通过,你的回调就是找好友聊天

比如失败,你的回调可能是自我分析是什么情况,也可以是什么都不做

在浏览器中,就是浏览器的异步请求取回结果的时候,如果是成功,要进行什么处理、给出什么输出呢?失败的话要不要处理、怎么处理这个错误呢?

举个例子,就是 setTimeout

1
2
const fn = ()=>console.log('success')
setTimeout(fn,1000)

执行 setTimeout 后,将在 1000ms 后执行 fn

此处的 fn 就是回调函数

异步和回调的关系

上文可以看出,回调函数其实就是事件完成后,处理结果的方法

但其实事件不一定是异步的,也可以是同步的

即便是同步的函数,也可以使用回调函数,区别只不过是异步任务执行后可以先做别的再收通知,同步任务执行后必须原地等通知罢了

但是如果一层层回调,会变成什么样呢

1
2
3
4
5
6
7
setTimeout(()=>{
setTimeout(()=>{
setTimeout(()=>{
run()
},0)
},0)
},0)

看,是不是随着回调层数的增加,代码逐渐向右突出。。。这也太难看了

这就是回调地狱

而为了解决这个问题,提出了 Promise

Promise 定义

Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。

用我们的类比来说:这就是就像是“订阅列表”。

“生产者代码”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在它准备好时,将结果向所有订阅了的代码开放。

Promise 用途

一开始主要是为了解决回调地狱而产生的

现在则已经是前端异步处理的统一解决方案

Promise 用法

一般在函数中作为函数返回值,例如

1
2
3
4
5
6
function task(){
return new Promise((resolve,reject)=>{
console.log('promise 正在运行')
resolve(233)
})
}

上述代码也可以直接赋值

1
2
3
4
let task = new Promise((resolve,reject)=>{
console.log('promise 正在运行')
resolve(233)
})

首先介绍一下,Promise 有以下状态

  1. pending,Promise 尚未返回结果
  2. settled,Promise 已经返回结果,但不确定是成功还是失败
    1. fulfilled,Promise 已经成功(resolve)返回
    2. rejected,Promise 已经失败(reject)返回

可见,Promise 的构造函数要求传入一个包含两个参数的 executor(可执行代码段),两个参数分别是 Promise 给出的两个函数,分别指示 resolve 和 reject 调用的函数

当然两个参数也可以叫别的名字,只要顺序不变就没有影响,但是一般都是叫 resolve 和 reject

这段 executor 是立即执行的,但是对结果的处理是异步的,直到 settled 之前,这个 Promise 都不算结束

即使在 executor 中,半路上就 resolve/reject 了,executor 的代码段还是会完整执行,不会中断

那么怎么处理 Promise 的返回结果呢?

Promise 方法

Promise 的 prototype 上有 3 个重要的回调方法

  1. then
  2. catch
  3. finally

3 个方法都支持链式调用

then

then 方法的函数声明伪代码如下

1
2
3
4
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
)

可以接受两个参数,分别表示对于 fulfilled 和 rejected 状态的 Promise 结果的处理函数

第一个函数接受一个参数,表示成功的结果

第二个函数接受一个参数,表示失败的原因

其中 fulfilled 结果处理函数是必要的,rejected 处理函数可以不写出,但如果 Promise 被 reject,而没有其它地方处理这个 error 的话,会直接报错,停止解释器

上例中的代码,加上 then 回调后,变为如下模样

1
2
3
4
5
6
let task = new Promise((resolve,reject)=>{
console.log('promise 正在运行')
resolve(233)
}).then(res=>{
console.log(res) // expected output: 233
})

then 可以多级传递,比如

1
2
3
4
5
6
7
8
9
let task = new Promise((resolve,reject)=>{
console.log('promise 正在运行')
resolve(233)
}).then(res=>{
console.log(res) // expected output: 233
return 114514
}).then(res=>{
console.log(res) // expected output: 114514
})

链式调用中,结果会沿着链传递

但是分开调用是不对的,比如

1
2
3
4
5
6
7
8
9
10
11
let task = new Promise((resolve,reject)=>{
console.log('promise 正在运行')
resolve(233)
})
task.then(res=>{
console.log(res) // expected output: 233
return 114514
})
task.then(res=>{
console.log(res) // expected output: 233
})

分开调用,结果不会互相传递

then 中除了被动出错,也可以使用 throw 子句主动抛出错误

catch

上例中使用 catch 的话,一般可以写成以下模样

1
2
3
4
5
6
7
8
let task = new Promise((resolve,reject)=>{ // executor
console.log('promise 正在运行')
reject(233)
}).then(res=>{ // then
console.log(res)
}).catch(err=>{
console.log(err)
})

如果在 Promise 的 executor 或者 then 中出错了,都可以用 catch 捕获错误,因为 catch 在他们的下游

此时如果是 executor 出错,then 会被跳过,沿着 Promise 链往下寻找 catch 直到找到第一个 catch 为止

catch 既然支持链式调用,那么 catch 自然也可以再抛出

比如

1
2
3
4
5
6
7
8
9
10
11
let task = new Promise((resolve,reject)=>{ // executor
console.log('promise 正在运行')
reject('2323')
}).then(res=>{ // then
console.log(res)
}).catch(err=>{
if(err!=='233') throw err
console.log('err 233 is catched')
}).catch(err=>{
console.log(err)
})

注意,此处要使用 throw 子句来抛出,不能是 return

如果是 return,接下来的结果就要用 then 来接收

finally

上例中使用 finally 的话,一般可以写成以下模样

1
2
3
4
5
6
7
8
9
let task = new Promise((resolve,reject)=>{ // executor
resolve(233)
}).finally(()=>{
console.log('promise 开始运行')
}).then(res=>{
console.log(res) // expected output: 233
}).catch(err=>{
console.log(err)
})

显然,finally 不一定要写在最后,一般只是用来做阶段性封口而已

比如此处,在 executor 执行完毕后,不论 executor 是否出错,都会执行 finally,告诉用户 promise 开始了

然后,不论是 resolve 还是 reject,结果都会越过 finally 传递到可以处理这个结果的回调


以上就是 Promise 的基本用法

现在来深究一下 Promise 这么个好东

Promise 手写

面试必考的手写 Promise 部分,你会多少呢?

Promise

首先先实现基本的 Promise 功能——resolve 和 reject

容易想到以下基本形

1
2
3
function P(executor){
executor(resolve,reject)
}

那么 resolve 和 reject 从哪里来?显然要由 Promise 提供

这两个函数还兼顾了取结果和改状态的效果,所以得到如下形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function P(executor){
this.status = 'pending'
this.result = undefined

try{
executor(this.resolve.bind(this),this.reject.bind(this))
} catch(err){
this.reject(err)
}
}
P.prototype.resolve = function(result){
if(this.status === 'pending'){
this.status = 'fulfilled'
this.result = result
}
}
P.prototype.reject = function(err){
if(this.status === 'pending'){
this.status = 'rejected'
this.result = err
}
}

修改数据的时候,要注意不能随便覆盖结果

注意,我们要在 resolve 和 reject 中使用访问 this 的 status 和 result,所以这两个函数

  1. 必须有自己的 this,不能是箭头函数
  2. 必须指向当前操作的对象,所以要使用 bind 绑定 this

这样 Promise 最基本的功能就实现了,接下来实现最重要的功能 then

Promise.then

容易得到以下形状

1
2
3
4
5
P.prototype.then = function(success,fail){
if(this.status==='fulfilled') this.result = success(this.result)
else this.result = fail(this.result)
return this
}

但是这时候我们发现,这个 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
function P(executor){
this.status = 'pending'
this.result = undefined

this.onfulfilled = []
this.onrejected = [] // 添加两个事件组

try{
executor(this.resolve.bind(this),this.reject.bind(this))
} catch(err){
this.reject(err)
}
}
P.prototype.resolve = function(result){
if(this.status === 'pending'){
this.status = 'fulfilled'
this.result = result
this.onfulfilled.map(event=>event.call(this))
}
}
P.prototype.reject = function(err){
if(this.status === 'pending'){
this.status = 'rejected'
this.result = err
this.onrejected.map(event=>event.call(this))
}
}
P.prototype.then = function(success,fail){
if(this.status === 'pending'){
this.onfulfilled.push(function(){
this.result = success(this.result)
})
this.onrejected.push(function(){
this.result = fail(this.result)
})
} else {
if(this.status === 'fulfilled') this.result = success(this.result)
else this.result = fail(this.result)
}
return this
}

运行测试例

1
2
3
4
5
6
7
8
let p=new P((resolve,reject)=>{
console.log('start')
setTimeout(()=>resolve(233),0)
console.log('end')
}).then(res=>{
console.log(res++)
return 666
}).then(res=>console.log(res))

可以得到以下结果

1
2
3
4
start
end
233
666

现在 then 也可以支持异步返回了!但是 catch 似乎还没实现,不过我水平不够了,到此为止了2333

Promise.all

Promise.all 的核心要义就是传入多个 Promise,然后按顺序返回所有结果

容易想到以下形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
P.all = function(promises){
return new P((resolve,reject)=>{
try{
let n = 0, len = promises.length
let ans = []
for(let i in promises){
P.resolve(promises[i])
.then(res=>{
ans[i] = res
n++
if(n === len){
resolve(ans)
}
})
}
}catch(err){
reject(err)
}
})
}

但是我们还需要一个 P.resolve 来得到一个新的 P

所以还要写

1
2
3
4
5
6
P.resolve = data => {
if(data instanceof P){
return data
}
return new P((resolve,reject)=>resolve(data))
}

运行以下测试例

1
2
3
4
5
6
7
8
9
let promise1 = P.resolve(3)
let promise2 = new P(function(resolve, reject) {
setTimeout(resolve, 100, 'foo')
})
let promise3 = 42

P.all([promise1, promise2, promise3]).then(function(values) {
console.log(values)
});

可得结果

1
[3, "foo", 42]

Promise.race

Promise.race 的核心要义就是传入多个 Promise,然后返回第一个决议的结果,无论是成功还是失败

其实把 all 的计数器去掉就可以了

容易想到以下形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
P.race = function(promises){
return new P((resolve,reject)=>{
try{
let ans = undefined
for(let i in promises){
P.resolve(promises[i])
.then(res=>{
ans = res
resolve(ans)
})
}
}catch(err){
reject(err)
}
})
}

运行以下测试例

1
2
3
4
5
6
7
8
9
10
11
let promise1 = new P(function(resolve, reject) {
setTimeout(resolve, 500, 'one');
})

let promise2 = new P(function(resolve, reject) {
setTimeout(resolve, 100, 'two');
})

P.race([promise1, promise2]).then(function(value) {
console.log(value)
})

可得结果

1
two

完整的 Promise

总结一下,可以得到如下的手写 Promise

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
function P(executor){
this.status = 'pending'
this.result = undefined

this.onfulfilled = []
this.onrejected = []

try{
executor(this.resolve.bind(this),this.reject.bind(this))
} catch(err){
this.reject(err)
}
}
P.prototype.resolve = function(result){
if(this.status === 'pending'){
this.status = 'fulfilled'
this.result = result
this.onfulfilled.map(event=>event.call(this))
}
}
P.prototype.reject = function(err){
if(this.status === 'pending'){
this.status = 'rejected'
this.result = err
this.onrejected.map(event=>event.call(this))
}
}
P.prototype.then = function(success,fail){
if(this.status === 'pending'){
this.onfulfilled.push(function(){
this.result = success(this.result)
})
this.onrejected.push(function(){
this.result = fail(this.result)
})
} else {
if(this.status === 'fulfilled') this.result = success(this.result)
else this.result = fail(this.result)
}
return this
}
P.resolve = data => {
if(data instanceof P){
return data
}
return new P((resolve,reject)=>resolve(data))
}
P.reject = data => {
if(data instanceof P){
return data
}
return new P((resolve,reject)=>reject(data))
}
P.all = function(promises){
return new P((resolve,reject)=>{
try{
let n = 0, len = promises.length
let ans = []
for(let i in promises){
P.resolve(promises[i])
.then(res=>{
ans[i] = res
n++
if(n === len){
resolve(ans)
}
})
}
}catch(err){
reject(err)
}
})
}
P.race = function(promises){
return new P((resolve,reject)=>{
try{
let ans = undefined
for(let i in promises){
P.resolve(promises[i])
.then(res=>{
ans = res
resolve(ans)
})
}
}catch(err){
reject(err)
}
})
}

参考链接

Promise

手写Promise.all和Promise.race


谢谢阅读

--It's the end.Thanks for your read.--