Promise设计思维——底层原理

「ES6」- 浅谈Promise设计思维 —— 源码

Posted by wendy on January 7, 2020
  • 什么是Promise
  • 传统异步解决方案
  • 认识Promise
  • 实现一个Promise

JavaScript作为单线程语言,其特点也是其缺陷,特点是不用处理多线程引发的占用资源、冲突等,缺陷就是同一时间,只能做一件事情。因此会存在一个问题,网络传输是有延迟的。比如A发一条信息给B, B还没返回信息给A, 那么A就会一直等待接受信息,会造成页面卡顿等问题。于是出现了异步。

什么是Promise

Promise是一个容器,保存着一个异步操作的结果。Promise是一个对象,从它可以获取异步操作的消息。Promise提供了统一的api, 各种异步操作都可以用同样的方法进行处理。

传统异步解决方案

一、callback回调

通过参数闯入回调,未来调用回调时让函数的调用者判断发生了什么?回调函数被认为是一种高级函数,一种被作为参数传递给另一个函数(在这称作”otherFunction”)的高级函数

1
2
3
4
5
6
7
<img src='../a.jpg'></img>
<img src='../b.jpg'></img>
<img src='../c.jpg'></img>

$("img").attr("title",function(index,attr){
    console.log(index) // 依次返回0 1 2
});
  • 优点:简单、容易理解和部署
  • 缺点:不利于代码的阅读和维护,程序流程混乱,每个任务只能指定一个回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
目录结构:
- iamswr
  - A.txt
  - B.txt
  - C.txt
  
  其中
  A.txt文件里的内容为字符串B.txt
  B.txt文件里的内容为字符串C.txt
  C.txt文件里的内容为字符串'hello swr'
  
  那么当我们想获取到'hello swr',会遇到什么问题呢?请看下面的代码
  let fs = require('fs')
  fs.readFile('A.txt','utf8',function(err,data){ // 此时回调函数data值为'B.txt'
      fs.readFile(data,'utf8',function(err,data){ // 此时回调函数data值为'C.txt'
          fs.readFile(data,'utf8',function(err,data){
            console.log(data) // 'hello swr'
          })
      })
  })

二、事件发布/订阅模式

回调函数的事件化,任务的执行不取决于代码的顺序,而取决于某个事件是否发生

  • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
  • 缺点:整个程序变成了事件驱动型,运行流程会变得很不清晰

三、Deferred延迟函数

通过deferred对象给异步操作进行状态绑定,deferred对象提供统一的api, 对各种异步操作状态进行操作(成功、失败、进行中)

  • 优点:避免了层层嵌套的回调函数deferred对象提供统一的接口,控制异步操作更容易;
  • 缺点:状态不可逆,从待定状态切换到任何一个确定状态后,再次调用resole和reject对愿状态将不起任何作用;

认识Promise

思考: 用同步化的方法来书写异步代码

异步操作的状态管理:成功\失败\进行中,每一个状态管理一个容器(3个),即成功之后的事情(找到对应的callback)、失败之后的事情(找到对应的callback)、进行中之后的事情(找到对应的callback)

  • 成功 resolve()
  • 失败 reject()
  • 进行中

知识普及:

闭包最大用处: 一个可以读取函数内部的变量,另一个就是让这些变量值保持在内存中; 思考:封闭作用域,保存作用域,作用域链条, 不污染全局变量、块级作用域

Promise的一些特性

  • promise是有兼容性问题的,node环境下默认支持,还可以下载相应插件来解决兼容性问题
  • promise是有三种状态的,等待态pending / 成功态resolved / 失败态rejected
  • promise的状态是可以转换的,可以从pending -> resolved 或 pending -> rejected,但是resolved不能转换为rejected/pending,rejected不能转换为resolved/pending,简而言之即状态只会更改一次
1
2
3
4
5
6
7
8
9
10
11
// Promise构造函数的第一个参数为executor
let promise = new Promise(function(resolve,reject){
    console.log('我是会被立即执行的哟')
})

// promise的实例都有then方法
promise.then(()=>{ // 成功的回调
    
},()=>{ // 失败的回调
    
})
  • executor默认在new的时候会自动执行
  • 每个promise的实例都有then方法
  • then方法中,有两个参数,分别是成功的回调函数和失败的回调函数
1
2
3
4
5
6
7
8
9
10
// 默认时为pending态,既不会走成功的回调也不会走失败的回调
promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})

console.log('2')

// 在这段代码中,只会打印出'2',因为promise一直处于pending态,不会走then后的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise(function(resolve,reject){
    console.log('1')
    resolve() // 更改pending状态为resolved
})

promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})
console.log('2')

// 此时输出顺序为'1' -> '2' -> 'success1'

then方法是异步的,属于微任务,从上面的例子可以看出,先执行完同步代码,再执行异步代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let promise = new Promise(function(resolve,reject){
    console.log('1')
    setTimeout(()=>{ // 异步行为
        resolve() // 更改状态为成功
    },1000)
})

promise.then(()=>{
    console.log("success1")
})

promise.then(()=>{
    console.log('success2')
})

console.log("2")

// 此时输出顺序为'1' -> '2' -> 'success1' -> 'success2'
  • 同一个promise的实例可以then多次,成功时会调用所有的成功方法,失败时会调用所有的失败方法
  • new Promise中可以支持异步行为
  • 如果发现错误,就会进入失败态
1
2
3
4
5
6
7
8
9
10
11
let promise = new Promise(function(resolve,reject){
    throw new Error('出错了') // 抛出错误
})

promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})

// 此时输出为 'error1'

实现一个Promise

基础结构

下面代码部分和源码实现部分要结合来看

1
2
3
4
5
6
7
8
9
10
11
12
13
// ----- 代码部分
// 1.executor默认在new的时候会自动执行
// 成功和失败的视乎可以传递参数
let promise = new Promise((resolve,reject)=>{ // 6.resolve、reject函数对应源码实现部分的resolve、reject函数
    resolve('hello swr') // 11.执行resolve
})

// 7.Promise的实例都有then方法
promise.then((data)=>{ // 8.成功的回调函数
    
},(err)=>{ // 9.失败的回调函数
    
})
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
// ----- 源码实现部分
// 2.声明一个Promise构造函数
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined // 12.因为value和reason值需要在Promise实例方法then中使用,所以把这两个值,赋给new出来的实例
    function resolve(value){ // 3.声明一个resolve函数
      if(self.status === 'pending'){ // 2.此时新增一个状态判断,当状态为pending的时候才能执行
        self.value = value // 13.当调用了resolve并且传参数时,则把这value值赋予self.value
        self.status = 'resolved' // 2.当调用了resolve时,更改状态为resolved
      }
    }
    
    function reject(reason){ // 4.声明一个reject函数
     if(self.status === 'pending'){ // 2.此时新增一个状态判断,当状态为pending的时候才能执行
        self.reason = reason // 13.当调用了reject并且传参数时,则把这reason值赋予self.reason
        self.status = 'rejected' // 2.当调用了reject时,更改状态为rejected
     }
    }
    // 5.当我们在执行executor时,内部抛出错误的时候,可以利用try catch来处理这个问题
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}
}

// 因为Promise的实例都有then方法,那么意味着then方法是在Promise的原型对象中的方法
// 10.对应上面成功的回调函数onFulfilled以及失败的回调函数onRejected
Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    // 3.当我们在then中,执行了成功或者失败的回调函数时,首先要判断目前处于什么状态
    if(self.status === 'resolved'){
        onFulfilled(self.value) // 4.当调用了resolve函数后,会执行成功的回调函数,并且把resolve中传递的值,传递给成功的回调函数
    }
    
    if(self.status === 'rejected'){
        onRejected(self.reason) // 4.当调用了reject函数后,会执行成功的回调函数,并且把reject中传递的值,传递给失败的回调函数
    }
}

module.exports = Promise // 把Promise暴露出去

思考:如果此时resolve或者reject是处于setTimeout(()=>{resolve()},3000)中,即处于异步中,当我们new一个Promise时,不会马上执行异步代码,而是直接执行了promise.then这个函数,而此时因为self.status的状态依然是处于pending,所以不会执行resolve或者reject,当同步代码执行完毕后,执行异步代码时,更改了状态为resolved或者rejected时,此时then方法已经执行完毕了,不会再次执行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
// ----- 源码实现部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending'
    self.onResolvedCallbacks = [] // 2.可能new Promise中会有异步的操作,此时我们把异步操作时,执行的then函数的成功回调,统一保存在该数组中
    self.onRejectedCallbacks = [] // 2.可能new Promise中会有异步的操作,此时我们把异步操作时,执行的then函数的失败回调,统一保存在该数组中
    function resolve(value){
        if(self.status === 'pending'){ 
            self.value = value 
            self.status = 'resolved'
            // 4.当调用resolve时,把该数组中存放的成功回调都执行一遍,如果是异步,则会把成功的回调都存到该数组里了,如果是异步,则没存到。
            self.onResolvedCallbacks.forEach(fn=>fn())
        }
    }
    
    function reject(reason){
        if(self.status === 'pending'){ 
            self.reason = reason 
            self.status = 'rejected'
            // 4.当调用reject时,把该数组中存放的失败回调都执行一遍,如果是异步,则会把成功的回调都存到该数组里了,如果是异步,则没存到。
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }
    
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    if(self.status === 'resolved'){
        onFulfilled(self.value) 
    }
    
    if(self.status === 'rejected'){
        onRejected(self.reason) 
    }
    
    // 3.当new Promise中有resolve、reject处于异步中,执行then的时候,状态为pending,
    if(self.status === 'pending'){
        self.onResolvedCallbacks.push(()=>{
            onFulfilled(self.value)
        }) // 3. 把成功的回调函数,存到该数组中,这样写的好处,就是把参数传进去,不需要将来遍历onResolvedCallbacks时,再传参
        self.onRejectedCallbacks.push(()=>{
            onRejected(self.reason)
        }) // 3. 把失败的回调函数,存到该数组中,这样写的好处,就是把参数传进去,不需要将来遍历onRejectedCallbacks时,再传参
    }
}

module.exports = Promise

思考:同一个promise的实例可以then多次,成功时会调用所有的成功方法,失败时会调用所有的失败方法,那这个又该如何处理呢?

Promise的链式调用

其实Promise的核心在于链式调用,Promise主要是解决2个问题:

  • 回调地狱
  • 并发异步io操作,同一时间内把这个结果拿到,即比如有两个异步io操作,当这2个获取完毕后,才执行相应的代码,比如前面所说的after函数,发布订阅、Promise.all等。

用Promise方式可以得出结论:

  • 1.如果一个promise执行完后,返回的还是一个promise,会把这个promise的执行结果会传递给下一次then中
  • 2.如果在then中返回的不是一个promise,而是一个普通值,会将这个普通值作为下次then的成功的结果
  • 3.如果当前then中失败了,会走下一个then的失败回调
  • 4.如果在then中不返回值,虽然没有显式返回,但是默认是返回undefined,是属于普通值,依然会把这个普通值传到下一个then的成功回调中
  • 5.catch是错误没有处理的情况下才会执行
  • 6.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
Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    let promise2 // 3.上面讲promise链式调用时,已经说了返回的是一个新的promise对象,那么我们声明一个新的promise
    
    // 4.那么我们new一个新的promise,并且把以下代码放到promise中
    let promise2 = new Promise((resolve,reject)=>{
        if(self.status === 'resolved'){
            // 7.当执行成功回调的时候,可能会出现异常,那么就把这个异常作为promise2的错误的结果
            try{
                let x = onFulfilled(self.value) // 6.这里的x,就是上面then中执行完返回的结果,我们在这里声明一个x用来接收
                // 8.根据promiseA+规范,我们应该提供一个函数来处理promise2
                //   我个人的理解是,then中不管是成功回调还是失败回调,其返回
                //   值,有可能是promise,也有可能是普通值,也有可能是抛出错误
                //   那么我们就需要一个函数来处理这几种不同的情况
                //   这个函数我们声明为resolvePromise吧
                resolvePromise(promise2,x,resolve,reject)
                // 9. 这里的promise2就是当前的promise2,x则是执行then中成功回调后返回的结果,如果是成功则调promise2的resolve,失败则调reject
            }catch(e){
                reject(e) // 注意:这里的reject是这个promise2的reject
            }
        }
        
        if(self.status === 'rejected'){
            // 同6-7步
            try{
                let x = onRejected(self.reason) 
                // 同8-9
                resolvePromise(promise2,x,resolve,reject)
            }catch(e){
                reject(e)
            }
        }
    
        if(self.status === 'pending'){
            self.onResolvedCallbacks.push(()=>{
                // 同6-7步
                try{
                    let x =  onFulfilled(self.value)
                    // 同8-9
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            }) 
            self.onRejectedCallbacks.push(()=>{
                // 同6-7步
                try{
                    let x = onRejected(self.reason)
                    // 同8-9
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            })
        }
    })
    return promise2 // 5.在jquery中是return this,但是在promise中,则是返回一个新的promise对象
}
写一个resolvePromise函数

接下来我们写一下resolvePromise这个函数,整个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
// 1.声明一个resolvePromise函数
// 这个函数非常核心,所有的promise都遵循这个规范,所有的promise可以通用,
/**
 * 
 * @param {*} promise2 then的返回值,返回新的promise
 * @param {*} x then中成功函数或者失败函数的返回值
 * @param {*} resolve promise2的resolve
 * @param {*} reject promise2的reject
 */
function resolvePromise(promise2,x,resolve,reject){
    // 3.从2中我们可以得出,自己不能等于自己
    // 当promise2和x是同一个对象的时候,则走reject
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle detected for promise'))
    }
    // 4.因为then中的返回值可以为promise,当x为对象或者函数,才有可能返回的是promise
    let called
    if(x !== null && (typeof x === 'object' || typeof x === 'function')){
        // 8.从第7步,可以看出为什么会存在抛出异常的可能,所以使用try catch处理
        try{
            // 6.因为当x为promise的话,是存在then方法的
            // 但是我们取一个对象上的属性,也有可能出现异常,我们可以看一下第7步
            let then = x.then 
            
            // 9.我们为什么在这里用call呢?解决了什么问题呢?可以看上面的第10步
            // x可能还是个promise,那么就让这个promise执行
            // 但是还是存在一个恶作剧的情况,就是{then:{}}
            // 此时需要新增一个判断then是否函数
            if(typeof === 'function'){
                then.call(x,(y)=>{ // y是返回promise后的成功结果
                    // 一开始我们在这里写的是resolve(y),但是考虑到一点
                    // 这个y,有可能还是一个promise,
                    // 也就是说resolve(new Promise(...))
                    // 所以涉及到递归,我们把resolve(y)改成以下
                    
                    // 12.限制既调resolve,也调reject
                    if(called) return
                    called = true
                    
                    resolvePromise(promise2,y,resolve,reject)
                    // 这样的话,代码会一直递归,取到最后一层promise
                    
                    // 11.这里有一种情况,就是不能既调成功也调失败,只能挑一次,
                    // 但是我们前面不是处理过这个情况了吗?
                    // 理论上是这样的,但是我们前面也说了,resolvePromise这个函数
                    // 是所有promise通用的,也可以是别人写的promise,如果别人
                    // 的promise可能既会调resolve也会调reject,那么就会出问题了,所以我们接下来要
                    // 做一下限制,这个我们写在第12步
                    
                },(err)=>{ // err是返回promise后的失败结果
                    if(called) return
                    called = true
                    reject(err)
                })
            }else{
                resolve(x) // 如果then不是函数的话,那么则是普通对象,直接走resolve成功
            }
        }catch(e){ // 当出现异常则直接走reject失败
            if(called) return
            called = true
            reject(e)
        }
    }else{ // 5.x为一个常量,则是走resolve成功
        resolve(x)
    }
}

完整代码 也顺便带大家理顺一下
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
133
134
135
136
137
138
139
function Promise(executor){
  let self = this;
  self.value = undefined; // 成功的值
  self.reason = undefined; // 失败的值
  self.status = "pending"; // 目前promise的状态为pending,还有resolved、rejected
  self.onResolvedCallbacks = []; // 可能new promise的时候存在异步操作,把成功和失败回调保存起来
  self.onRejectedCallbacks = [];

  // 把状态更改为成功
  function resolve(value){ 
    // 只有pending状态才能转为成功状态
    if(self.status === 'pending'){
      self.value = value;
      self.status = "resolved";
      self.onResolvedCallbacks.forEach(fn => fn());
    }
  }

  function reject(reason){
    if(self.status === 'pending'){
      self.status = "rejected"
      self.reason = reason;
      self.onRejectedCallbacks.forEach(fn => fn());
    }
  }

  try {
    executor(resolve, reject);
  } catch(error) {
    reject(error);
  }
}

/**
 * 所有的promise都遵循这个规范,所有的promise可以通用
 * @param {*} promise2  then的返回值,返回新的promise
 * @param {*} x then中成功函数或失败函数的返回值
 * @param {*} resolve  promise2的resolve
 * @param {*} reject promise2的reject
 */
function resolvePromise(promise2, x, resolve, reject){
  // promise2和x是同一个对象,reject
  if(promise2 === x){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  // then的返回值是promise,当x为对象或者函数,才可能是promise
  if(x!==null && (typeof x === 'object' || typeof x === 'function')){

    try{
      let then = x.then;
      // x可以还是个promise,排除{then:{}}
      if(typeof then === 'function'){
        then.call(x, (y)=>{
          // y是返回promise后成功结果
          // 如果别人的promise可能既会调resolve也会调reject,那么就会出问题了,所以我们接下来要限制
          if(called) return;
          called = true;
          // 代码会一直递归,取到最后一层promis
          resolvePromise(promise2, y, resolve, reject);
       
        },(err)=>{
          // 返回promise失败结果
          if(called) return;
          called = true;
          reject(err);
        });
      }else{
        resolve(x) // 如果then不是函数的话,那么则是普通对象,直接走resolve成功
      }
    }catch(e){
      if(called) return;
      called = true;
      reject(e);
    }
  }else{
    // x为一个常量,则是走resolve成功
    resolve(x);
  }
}

Promise.prototype.then = function(onFullfilled, onRejected){
  let self = this;
  // 那么我们new一个新的promise,并且把以下代码放到promise中
  let promise2 = new Promise((resolve, reject)=>{
    if(self.status === 'resolved'){
      try {
        // 这里的x, 上一层then执行返回的结果
        let x = onFullfilled(self.value);
        // 提供一个resolvePromise函数处理promise2, then不管是成功回调、失败回调,返回值可能为promise/普通值/抛出错误值
        resolvePromise(promise2, x, resolve, reject);
      } catch(e){
        reject(e);
      }
    }
    if(self.status === 'rejected'){
      try {
        let x =  onRejected(self.reason);
        resolvePromise(promise2, x, resolve, reject);
      } catch(e){
        reject(e);
      }
    }
    if(self.status === 'pending'){
      // 把成功回调函数,存到改数组中,把参数传进去,不需要将来遍历onResolvedCallbacks时,再传参
      self.onResolvedCallbacks.push(()=> {
        try {
            let x = onFullfilled(self.value);
            resolvePromise(promise2, x, resolve, reject);
        } catch(e){
          reject(e);
        }
      });
      self.onRejectedCallbacks.push(() => {
        try {
          let x =  onRejected(self.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch(e){
          reject(e);
        }
      });
    }
  });
  return promise2;
}

// 是否为模块引入,健壮性判断
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  module.exports = Promise;
} else {
  if (typeof define === 'function' && define.amd) {
    define([], function() {
      return Promise;
    });
  } else {
    window.Promise = Promise;
  }
}

常用的promise方法

接下来,我们完善一下这个promise,写一下常用的promise方法

  • Promise.reject
  • Promise.resolve
  • catch
  • Promise.all
  • Promise.race
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
// 简化的promise, 语法糖可以简化一些操作
Promise.defer = Promise.deferred = function(){
  let dfd = {};
  dfd.promise = new Promise((resolve, reject)=>{
    dfd.resolve =resolve;
    dfd.reject =reject;
  });
  return dfd;
}


// 原生的Promise.resolve使用
Promise.resolve = function(value){
  return new Promise((resolve, reject)=>{
    resolve(value);
  });
}

Promise.reject = function(reason){
  return new Promise((resolve, reject)=>{
    reject(reason);
  });
}

Promise.prototype.catch = function(onRejected){
  return this.then(null, onRejected);
}

Promise.all = function(promises){
  return new Promise((resolve, reject)=>{
    let arr = [];
    let i = 0;
    function processData(index, data){
      arr[index] = data;
       // 所有结束时候
      if(++i === promises.length){
        resolve(arr);
      }
    }
    for(let i = 0; i < promises.length; i++){
      promises[i].then((data)=> processData(i, data), reject);
    }
  });
}

Promise.race = function(promises){
  return  new Promise((resolve, reject)=>{
    for(let i = 0; i < promises.length; i++){
      promises[i].then(resolve, reject);
    }
  });
}

结尾:希望对大家有所帮助,promise的源码实现,我最大的收获并不是怎么实现promise,而是编程思维,大家可以多多往深里想一想,也希望大家可以和我进行交流,共同进步