由淺入深,從掌握Promise的基本使用到手寫Promise

MomentYY發表於2022-04-03

由淺入深,從掌握Promise的基本使用到手寫Promise

前言

在ES6之前,對於一些非同步任務的處理始終沒有很好的方案可以解決,處理非同步的方案可謂是十分混亂,在業務需求下非同步請求的套用,就形成了回撥地獄,嚴重影響程式碼的閱讀性。而Promise的出現,給我們統一了規範,解決了之前處理非同步任務的許多痛點,並且它友好的使用方式,使之成為了JavaScript一大重點,同時也是面試的高頻問點,下面就一起來全面認識一下Promise吧。

1.什麼是Promise?

如果我們想在一個非同步請求之後,拿到請求的結果,在ES6之前我們可以怎麼做呢?

比如,給定一個請求地址,希望拿到它請求成功或者失敗的結果:

  • 可以通過分別設定成功和失敗的兩個回撥
  • 當請求成功後呼叫成功的回撥,將成功的結果傳遞過去;
  • 當請求失敗後呼叫失敗的回撥,將失敗的結果傳遞過去;
function request(url, successCb, failCb) {
  setTimeout(function() {
    if (url === '/aaa/bbb') { // 請求成功
      let res = [1, 2, 3]
      successCb(res)
    } else { // 請求失敗
      let err = 'err message'
      failCb(err)
    }
  })
}

// 呼叫方式,從回撥中拿結果
request('/aaa/bbb', function(res) {
  console.log(res)
}, function(err) {
  console.log(err)
})

將上面的情況使用Promise來實現一下:

  • Promise是一個類,通過new呼叫,可以給予呼叫者一個承諾;
  • 通過new建立Promise物件時,需要傳入一個回撥函式,這個回撥函式稱之為executor,executor接收兩個引數resolve和reject;
  • 傳入的回撥會被立即執行,當呼叫resolve函式時,會去執行Promise物件的then方法中傳入的成功回撥;
  • 當呼叫reject函式時,會去執行Promise物件的then方法中傳入的失敗回撥函式,並且請求後的結果可以通過引數傳遞過去;
function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url === '/aaa/bbb') {
        let res = [1, 2, 3]
        resolve(res) // 請求成功呼叫resolve
      } else {
        let err = 'err message'
        reject(err) // 請求失敗呼叫reject
      }
    })
  })
}

const p = request('/aaa/bbb')

p.then(res => {
  console.log(res) // 拿到resolve傳遞過來的值
}, err => {
  console.log(err) // 拿到reject傳遞過來的值
})

2.Promise的三種狀態

為什麼Promise能夠將請求的結果準確的傳遞到then中的回撥函式中,因為Promise其核心就用三種狀態來進行管控。

  • 待定狀態(pending):Promise的初始狀態;
  • 已兌現(resolved、fulfilled):操作成功,如執行resolve時就變為該狀態;
  • 已拒絕(rejected):操作失敗,如執行reject時就變為該狀態;

通過上面的案例,可以在瀏覽器中檢視Promise分別在執行resolve和reject後的列印結果和Promise當時處於的狀態:

  • resolve和reject都沒執行:

  • 執行resolve,請求成功:

  • 執行reject,請求失敗:

注意:在後續的對Promise的講述過程中,都需要帶著Promise的狀態去理解。

3.executor

executor是在建立Promise是需要傳入的一個回撥函式,這個回撥函式會被立即執行,並且傳入兩個引數,分別就是resolve和reject。

new Promise((resolve, reject) => {
  console.log('我是executor中的程式碼,我會被立即執行~')
})

通常我們會在executor中確定Promise的狀態,而且狀態一旦被確定下來,Promise的狀態就會被鎖死,即Promise的狀態一旦修改,就不能再次更改了

  • 當呼叫resolve,如果resolve傳入的值不是一個Promise(即傳入的值為一個普通值),Promise的狀態就會立即變成fulfilled;
  • 但是,如果在resolve後接著呼叫reject,是不會有任何的效果的,因為reject已經無法改變Promise的結果了;

4.resolve的引數

上面聊到了resolve需要傳入一個普通值,Promise的狀態才會被立即鎖定為fulfilled,那麼如果傳遞的不是普通值呢?一般resolve傳遞以下三類值,會有不同的表現效果。

  • 傳值一:resolve傳入一個普通值或普通物件,那麼這個值會作為then中第一個回撥的引數;

    const p = new Promise((resolve, reject) => {
      resolve(123)
    })
    
    p.then(res => {
      console.log(res) // 123
    })
    
  • 傳值二:resolve傳入一個Promise,那麼這個傳入的Promise會決定原來Promise的狀態;

    • 傳入的Promise呼叫的是resolve;

      const newP = new Promise((resolve, reject) => {
        resolve(123)
      })
      
      const p = new Promise((resolve, reject) => {
        resolve(newP)
      })
      
      p.then(res => {
        console.log(res) // 123
      }, err => {
        console.log(err)
      })
      
    • 傳入的Promise呼叫的是reject;

      const newP = new Promise((resolve, reject) => {
        reject('err message')
      })
      
      const p = new Promise((resolve, reject) => {
        resolve(newP)
      })
      
      p.then(res => {
        console.log(res)
      }, err => {
        console.log(err) // err message
      })
      
  • 傳值三:resolve傳入一個特殊物件,該物件中實現了then方法,那麼Promise的狀態就是物件中then方法執行後的結果來決定的;

    • then中執行了resolve;

      const obj = {
        then: function(resolve, reject) {
          resolve(123)
        }
      }
      
      const p = new Promise((resolve, reject) => {
        resolve(obj)
      })
      
      p.then(res => {
        console.log(res) // 123
      }, err => {
        console.log(err)
      })
      
    • then中執行了reject;

      const obj = {
        then: function(resolve, reject) {
          reject('err message')
        }
      }
      
      const p = new Promise((resolve, reject) => {
        resolve(obj)
      })
      
      p.then(res => {
        console.log(res)
      }, err => {
        console.log(err) // err message
      })
      

5.Promise相關例項方法

Promise的例項方法,就是可以通過其例項物件進行呼叫的方法。

5.1.then方法

then方法是Promise例項物件上的一個方法:Promise.prototype.then

(1)then方法接收兩個引數

  • 狀態變成fulfilled的回撥函式;
  • 狀態變成rejected的回撥函式;
promise.then(res => {
  console.log('狀態變成fulfilled回撥')
}, err => {
  console.log('狀態變成rejected回撥')
})

(2)then方法多次呼叫

  • 一個Promise的then方法是可以被多次呼叫的,每次呼叫都可以傳入對應的fulfilled回撥;
  • 當Promise的狀態變成fulfilled的時候,這些回撥函式都會被執行;
  • 反之,當Promise的狀態變成rejected,所有then中傳入的rejected回撥都會被執行;
const p = new Promise((resolve, reject) => {
  resolve('aaa')
})

p.then(res => {
  console.log(res) // aaa
})
p.then(res => {
  console.log(res) // aaa
})
p.then(res => {
  console.log(res) // aaa
})

(3)then方法中的返回值

then呼叫本身是有返回值的,並且它的返回值是一個Promise,所以then可以進行鏈式呼叫,但是then方法呼叫的返回值的狀態是什麼呢?主要是由其返回值決定的。

  • 當then方法中的回撥在執行時處於pending狀態;

  • 當then方法中的回撥返回一個結果時處於fulfilled狀態,並且會將結果作為resolve的引數;

    • 返回一個普通的值:這個普通的值會被作為一個新Promise的resolve中的值

      p.then(res => {
        return 123
        // 相當於:
        /*
          return new Promise((resolve, reject) => {
            resolve(123)
          })
        */
      }).then(res => {
        console.log(res) // 123
      })
      
    • 返回一個實現了then方法的物件:

      p.then(res => {
        const obj = {
          then: function(resolve, reject) {
            resolve('abc')
          }
        }
        return obj
        // 相當於:
        /*
          return new Promise((resolve, reject) => {
            resolve(obj.then)
          })
        */
      }).then(res => {
        console.log(res) // abc
      })
      
    • 返回一個Promise:

      p.then(res => {
        const newP = new Promise((resolve, reject) => {
          resolve(123)
        })
        return newP
        // 相當於:
        /*
          const newP = new Promise((resolve, reject) => {
            resolve(123)
          })
          return new Promise((resolve, reject) => {
            resolve(newP)
          })
        */
      }).then(res => {
        console.log(res) // 123
      })
      
  • 當then方法執行時丟擲一個異常,就處於rejected狀態,同樣,Promise的executor在執行的時候丟擲異常,Promise對應的狀態也會變成rejected;

    const p = new Promise((resolve, reject) => {
      throw new Error('err message')
    })
    
    p.then(res => {
      console.log(res)
    }, err => {
      console.log(err) // Error: err message
      return new Error('then err message')
    }).then(res => {
      console.log(res)
    }, err => {
      console.log(err) // Error: then err message
    })
    

5.2.catch方法

catch方法是Promise例項物件上的一個方法:Promise.prototype.catch

(1)catch方法可多次呼叫

  • 一個Promise的catch方法也是可以被多次呼叫的,每次呼叫都可以傳入對應的reject回撥;
  • 當Promise的狀態變成rejected的時候,這些回撥就都會執行;
  • catch方法的效果和then方法的第二個回撥函式差不多,可用於替代then方法的第二個回撥;
const p = new Promise((resolve, reject) => {
  reject('err message')
})

p.catch(err => {
  console.log(err) // err message
})
p.catch(err => {
  console.log(err) // err message
})
p.catch(err => {
  console.log(err) // err message
})

(2)catch方法的返回值

  • catch方法的執行返回的也是一個Promise物件,使用catch後面可以繼續呼叫then方法或者catch方法;

  • 如果在catch後面呼叫then方法,會進入到then方法的fulfilled回撥函式中,因為catch返回的Promise預設是fulfilled;

    p.catch(err => {
      return 'catch return value'
    }).then(res => {
      console.log(res) // catch return value
    })
    
  • 如果catch後續又呼叫了catch,那麼可以丟擲一個異常,就會進入後面的catch回撥中;

    p.catch(err => {
      throw new Error('catch err message')
    }).catch(err => {
      console.log(err) // Error: catch err message
    })
    

(3)catch的作用

  • catch主要是用於捕獲異常的,當executor丟擲異常是,可以通過catch進行處理;

  • 注意:當Promise的executor執行reject或者丟擲異常,後續必須要有捕獲異常的處理,如下程式碼,雖然都呼叫了then方法,接著後續又呼叫了catch方法,但是then和catch是兩次獨立的呼叫,兩次呼叫並沒有聯絡,所以就被認定為沒有處理異常。

    const p = new Promise((resolve, reject) => {
      reject('err message')
    })
    
    p.then(res => {
      console.log(res)
    })
    
    p.catch(err => {
      console.log(err)
    })
    

  • 正確處理的方法為:

    // 方法一:
    p.then(res => {
      console.log(res)
    }).catch(err => {
      console.log(err)
    })
    
    // 方法二:
    p.then(res => {
      console.log(res)
    }, err => {
      console.log(err)
    })
    

5.3.finally方法

finally方法是Promise例項物件上的一個方法:Promise.prototype.finally

  • finally是在ES9中新增的,無論Promise的狀態變成fulfilled還是rejected,最終都會執行finally中的回撥;
  • 注意finally是不接收引數的,因為它必定執行;
const p = new Promise((resolve, reject) => {
  resolve(123)
})

p.then(res => {
  console.log(res) // 123
}).catch(err => {
  console.log(err)
}).finally(() => {
  console.log('finally code') // finally code
})

6.Promise相關類方法

Promise的類方法,就是直接通過Promise進行呼叫。

6.1.resolve方法

resolve方法具體有什麼用呢?當我們希望將一個值轉成Promise來使用,就可以通過直接呼叫resolve方法來實現,其效果就相當於在new一個Promise時在executor中執行了resolve方法。

resolve傳入的引數型別:

  • 引數為一個普通的值;

    const p = Promise.resolve('aaaa')
    // 相當於:
    /*
      const p = new Promise((resolve, reject) => {
        resolve('aaaa')
      })
    */
    console.log(p)
    

  • 引數為一個實現了then方法的物件;

    const p = Promise.resolve({
      then: function(resolve, reject) {
        resolve('aaaa')
      }
    })
    // 相當於:
    /*
      const p = new Promise((resolve, reject) => {
        resolve({
          then: function(resolve, reject) {
            resolve('aaaa')
          }
        })
      })
    */
    console.log(p)
    

  • 引數為一個Promise;

    const p = Promise.resolve(new Promise((resolve, reject) => {
      resolve('abc')
    }))
    // 相當於:
    /*
      const p = new Promise((resolve, reject) => {
        resolve(new Promise((resolve, reject) => {
          resolve('abc')
        }))
      })
    */
    console.log(p)
    

6.2.reject方法

reject方法和resolve的用法一致,只不過是將可以得到一個狀態為rejected的Promise物件,並且reject不過傳入的是什麼引數,都會原封不動作為rejected狀態傳遞到catch中。

// 1.傳入普通值
const p1 = Promise.reject(123)
p1.then(res => {
  console.log(res)
}).catch(err => {
  console.log('err:', err)
})

// 2.傳入實現then方法物件
const p2 = Promise.reject({
  then: function(resolve, reject) {
    resolve('aaaa')
  }
})
p2.then(res => {
  console.log(res)
}).catch(err => {
  console.log('err:', err)
})

// 3.傳入Promise
const p3 = Promise.reject(new Promise((resolve, reject) => {
  resolve('aaaa')
 })
)
p3.then(res => {
  console.log(res)
}).catch(err => {
  console.log('err:', err)
})

6.3.all方法

all方法可以接收由多個Promise物件組成的陣列(準確來說是可接收一個可迭代物件),all方法呼叫返回的Promise狀態,由所有Promise物件共同決定。

  • 當傳入的所有Promise物件的狀態都為fulfilled是,all方法返回的Promise狀態就為fulfilled,並且會將所有Promise物件的返回值組成一個陣列

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(111)
      }, 1000)
    })
    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(222)
      }, 2000)
    })
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(333)
      }, 3000)
    })
    
    Promise.all([p1, p2, p3]).then(res => {
      console.log('res:', res) // res: [ 111, 222, 333 ]
    }).catch(err => {
      console.log('err:', err)
    })
    
  • 當傳入的Promise有一個變成了rejected狀態,那麼就會獲取第一個變成rejected狀態的返回值作為all方法返回的Promise狀態;

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(111)
      }, 1000)
    })
    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('err message')
      }, 2000)
    })
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(333)
      }, 3000)
    })
    
    Promise.all([p1, p2, p3]).then(res => {
      console.log('res:', res)
    }).catch(err => {
      console.log('err:', err) // err: err message
    })
    

6.4.allSettled方法

相比於all方法,allSettled方法不管傳入的Promise物件的狀態是fulfilled還是rejected,最終都會講結果返回,並且返回的結果是一個陣列,陣列中存放著每一個Promise對應的狀態status和對應的值value。

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(111)
  }, 1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err message')
  }, 2000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(333)
  }, 3000)
})

Promise.allSettled([p1, p2, p3]).then(res => {
  console.log('res:', res)
}).catch(err => {
  console.log('err:', err)
})

6.5.race方法

race翻譯為競爭,顧名思義哪一個Promise物件最先返回結果,就使用最先返回結果的Promise狀態。

一下程式碼是p1最先有結果的,p1中執行的是resolve,所以返回的狀態為fulfilled:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(111)
  }, 1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err message')
  }, 2000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(333)
  }, 3000)
})

Promise.race([p1, p2, p3]).then(res => {
  console.log('res:', res) // res: 111
}).catch(err => {
  console.log('err:', err)
})

6.6.any方法

any方法是ES12中新增的方法,與race是類似的,any方法會等到有一個fulfilled狀態的Promise,才會決定any呼叫返回新Promise的狀態(也就是說any一定會等到有一個Promise狀態為fullfilled)。

那麼,如果所有的Promise物件的狀態都變為了rejected呢?最終就會報一個AggregateError錯誤,如果想拿到所有的rejected狀態的返回值,可以通過在捕獲異常回撥引數中的errors獲取:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err message1')
  }, 1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err message2')
  }, 2000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err message3')
  }, 3000)
})

Promise.any([p1, p2, p3]).then(res => {
  console.log('res:', res)
}).catch(err => {
  console.log(err)
  console.log(err.errors)
})

注意:any方法是ES12新增的,node版本過低的話是會報錯找不到any方法的,可以在瀏覽器中測試。

7.手寫Promise

掌握了以上Promise的用法,那麼就一步步來實現一下Promise吧。

7.1.executor的實現

  • 建立一個類,這個類可接收一個executor函式;
  • executor函式需傳入兩個函式resolve和reject,並且executor是需要立即執行的;
  • 建立三個常量用於管理Promise的三種狀態;
  • 一旦Promise的狀態改變就不能再次被修改
  • 還需將傳入resolve和reject的引數值進行儲存,便於後續then的使用;
// 定義Promise的三種狀態常量
const PENDING_STATUS = 'pending'
const FULFILLED_STATUS = 'fulfilled'
const REJECTED_STATUS = 'rejected'

class MyPromise {
  constructor(executor) {
    // 初始化Promise的狀態為pending
    this.promiseStatus = PENDING_STATUS
    // 初始化變數,用於儲存resolve和reject傳入的引數值
    this.value = undefined
    this.reason = undefined

    // 1.定義executor需要傳入的resolve函式
    const resolve = (value) => {
      // 只有當Promise的狀態為pending,才能將狀態改變fulfilled
      if (this.promiseStatus === PENDING_STATUS) {
        this.promiseStatus = FULFILLED_STATUS
        this.value = value
        console.log('呼叫了resolve,狀態變成fulfilled啦~')
      }
    }

    // 2.定義executor需要傳入的reject函式
    const reject = (reason) => {
      // 只有當Promise的狀態為pending,才能將狀態改變為rejected
      if (this.promiseStatus === PENDING_STATUS) {
        this.promiseStatus = REJECTED_STATUS
        this.reason = reason
        console.log('呼叫了reject,狀態變成rejected啦~')
      }
    }

    // 3.將定義的兩個函式傳入executor並呼叫
    executor(resolve, reject)
  }
}

簡單測試一下:

// 先呼叫resolve
new MyPromise((resolve, reject) => {
  resolve()
  reject()
})
// 先呼叫reject
new MyPromise((resolve, reject) => {
  reject()
  resolve()
})

7.2.then方法的實現

(1)then基本實現

  • then方法接收兩個引數:
    • onFulfilled回撥:當Promise狀態變為fulfilled需要執行的回撥;
    • onRejected回撥:當Promise狀態變為rejected需要執行的回撥
class MyPromise {
  constructor(executor) {
    // 初始化Promise的狀態為pending
    this.promiseStatus = PENDING_STATUS
    // 初始化變數,用於儲存resolve和reject傳入的引數值
    this.value = undefined
    this.reason = undefined

    // 1.定義executor需要傳入的resolve函式
    const resolve = (value) => {
      // 只有當Promise的狀態為pending,才能將狀態改變fulfilled
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = FULFILLED_STATUS
          this.value = value
          // 狀態變成fulfilled就去呼叫onFulfilled
          this.onFulfilled(this.value)
        })
      }
    }

    // 2.定義executor需要傳入的reject函式
    const reject = (reason) => {
      // 只有當Promise的狀態為pending,才能將狀態改變為rejected
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = REJECTED_STATUS
          this.reason = reason
          // 狀態變成rejected就去呼叫onRejected
          this.onRejected(this.reason)
        })
      }
    }

    // 3.將定義的兩個函式傳入executor並呼叫
    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    // 儲存fulfilled和rejected狀態的回撥
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}

注意:這裡將onFulfilled和onRejected的調動放在了queueMicrotask,在JavaScript中可以通過queueMicrotask使用微任務,而原Promise的then中回撥的執行,也是會被放在微任務中的,為什麼要放在微任務中呢?

原因:如果不使用微任務,那麼在executor中執行resolve或者reject時,then方法還沒被呼叫,onFulfilled和onRejected就都還沒被賦值,所以呼叫時會報錯,加入微任務就可以實現將onFulfilled和onRejected的呼叫推遲到下一次事件迴圈,也就是等then呼叫後賦值了才會執行。

簡單測試一下:

const p1 = new MyPromise((resolve, reject) => {
  resolve('aaaa')
})
const p2 = new MyPromise((resolve, reject) => {
  reject('err message')
})

p1.then(res => {
  console.log(res) // aaaa
}, err => {
  console.log(err)
})

p2.then(res => {
  console.log(res)
}, err => {
  console.log(err) // err message
})

(2)then優化一

  • 對於以上then的基本實現,還存在一些不足之處,比如:
    • then方法是可以進行多次呼叫的,並且每一次呼叫都是獨立呼叫,互不影響,所以需要收集當Promise狀態改變時,對應需要執行哪些回撥,需用陣列進行收集;
    • 如果then是放到定時器中呼叫的,那麼改then的回撥是不會被呼叫的,因為在前面我們是通過將回撥新增到微任務中執行的,而定時器是巨集任務,會在微任務執行完成後執行,所以定時器中then的回撥就沒有被呼叫;
    • 當then是放到定時器中執行的,那麼執行的時候,微任務已經執行完成了,Promise狀態肯定也確定下來了,那麼只需要直接呼叫then中的回撥即可;
class MyPromise {
  constructor(executor) {
    // 初始化Promise的狀態為pending
    this.promiseStatus = PENDING_STATUS
    // 初始化變數,用於儲存resolve和reject傳入的引數值
    this.value = undefined
    this.reason = undefined
    // 初始化兩個陣列,分別用於儲存then中對應需要執行的回撥
    this.onFulfilledFns = []
    this.onRejectedFns = []

    // 1.定義executor需要傳入的resolve函式
    const resolve = (value) => {
      // 只有當Promise的狀態為pending,才能將狀態改變fulfilled
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = FULFILLED_STATUS
          this.value = value
          // 狀態變成fulfilled就去遍歷呼叫onFulfilled
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        })
      }
    }

    // 2.定義executor需要傳入的reject函式
    const reject = (reason) => {
      // 只有當Promise的狀態為pending,才能將狀態改變為rejected
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = REJECTED_STATUS
          this.reason = reason
          // 狀態變成rejected就去遍歷呼叫onRejected
          this.onRejectedFns.forEach(fn => {
            fn(this.reason)
          })
        })
      }
    }

    // 3.將定義的兩個函式傳入executor並呼叫
    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    // 1.如果在呼叫then時,Promise的狀態已經確定了,就直接執行回撥
    if (this.promiseStatus === FULFILLED_STATUS && onFulfilled) {
      onFulfilled(this.value)
    }
    if (this.promiseStatus === REJECTED_STATUS && onRejected) {
      onRejected(this.reason)
    }

    // 2.如果呼叫then時,Promise的狀態還沒確定下來,就放入微任務中執行
    if (this.promiseStatus === PENDING_STATUS) {
      // 將then中成功的回撥和失敗的回撥分別存入陣列中
      this.onFulfilledFns.push(onFulfilled)
      this.onRejectedFns.push(onRejected)
    }
  }
}

簡單測試一下:

const p = new MyPromise((resolve, reject) => {
  resolve('aaaa')
  reject('err message')
})

p.then(res => {
  console.log('res1:', res)
}, err => {
  console.log('err1:', err)
})

p.then(res => {
  console.log('res2:', res)
}, err => {
  console.log('err1:', err)
})

setTimeout(() => {
  p.then(res => {
    console.log('res3:', res)
  }, err => {
    console.log('err1:', err)
  })
})

(3)then優化二

  • 通過上一步優化,then方法還存在一個缺陷,就是不能進行鏈式呼叫,在前面講then方法時,then方法執行的返回值是一個promise物件,並且返回的promise狀態是由then方法中回撥函式的返回值決定的,then中必定需要返回一個新的Promise
  • 上一個then中回撥的返回值可以傳遞到下一個then中成功的回撥中,也就是返回的promise執行了resolve方法,那麼什麼時候可以傳遞到下一個then中失敗的回撥中呢?只需要上一個then中丟擲異常即可,相當於返回的promise執行了reject方法;
  • 所以,在這裡需要拿到then中回撥函式返回的結果,並且需要通過try catch判斷是呼叫resolve還是reject;
  • 注意:如果是在executor中就丟擲了異常,也需要通過try catch去執行executor;
class MyPromise {
  constructor(executor) {
    // 初始化Promise的狀態為pending
    this.promiseStatus = PENDING_STATUS
    // 初始化變數,用於儲存resolve和reject傳入的引數值
    this.value = undefined
    this.reason = undefined
    // 初始化兩個陣列,分別用於儲存then中對應需要執行的回撥
    this.onFulfilledFns = []
    this.onRejectedFns = []

    // 1.定義executor需要傳入的resolve函式
    const resolve = (value) => {
      // 只有當Promise的狀態為pending,才能將狀態改變fulfilled
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = FULFILLED_STATUS
          this.value = value
          // 狀態變成fulfilled就去遍歷呼叫onFulfilled
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        })
      }
    }

    // 2.定義executor需要傳入的reject函式
    const reject = (reason) => {
      // 只有當Promise的狀態為pending,才能將狀態改變為rejected
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = REJECTED_STATUS
          this.reason = reason
          // 狀態變成rejected就去遍歷呼叫onRejected
          this.onRejectedFns.forEach(fn => {
            fn(this.reason)
          })
        })
      }
    }

    // 3.將定義的兩個函式傳入executor並呼叫
    // 如果executor中就丟擲了異常,那麼直接執行reject即可
    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      // 1.如果在呼叫then時,Promise的狀態已經確定了,就直接執行回撥
      if (this.promiseStatus === FULFILLED_STATUS && onFulfilled) {
        // 通過try catch捕獲異常,沒有捕獲到執行resolve,捕獲到執行reject
        try {
          const value = onFulfilled(this.value)
          resolve(value)
        } catch (err) {
          reject(err)
        }
      }
      if (this.promiseStatus === REJECTED_STATUS && onRejected) {
        try {
          const reason = onRejected(this.reason)
          resolve(reason)
        } catch(err) {
          reject(err)
        }
      }

      // 2.如果呼叫then時,Promise的狀態還沒確定下來,就放入微任務中執行
      if (this.promiseStatus === PENDING_STATUS) {
        // 將then中成功的回撥和失敗的回撥分別存入陣列中
        // 將傳入的回撥外包裹一層函式,目的是為了這裡能夠拿到then中回撥執行的結果
        this.onFulfilledFns.push(() => {
          try {
            const value = onFulfilled(this.value)
            resolve(value)
          } catch(err) {
            reject(err)
          }
        })
        this.onRejectedFns.push(() => {
          try {
            const reason = onRejected(this.reason)
            resolve(reason)
          } catch(err) {
            reject(err)
          }
        })
      }
    })
  }
}

簡單測試一下:

const p = new MyPromise((resolve, reject) => {
  resolve('aaaa')
})

p.then(res => {
  console.log('res1:', res)
  return 'bbbb'
}, err => {
  console.log('err1:', err)
}).then(res => {
  console.log('res2:', res)
  throw new Error('err message')
}, err => {
  console.log('err2:', err)
}).then(res => {
  console.log('res3:', res)
}, err => {
  console.log('err3:', err)
})

7.3.catch方法的實現

  • catch方法的功能類似於then方法中的失敗回撥,所以,實現catch方法只需要呼叫then,給then傳入失敗的回撥即可;
  • 只給then傳入一個回撥,意味著根據上面的程式碼,我們還需要對then的回撥進行條件判斷,有值才新增到對應陣列中;
  • 注意:在then後鏈式呼叫catch會有一個問題,呼叫catch方法的promise是then執行之後返回的新promise,而catch真正需要去呼叫的是當前then的失敗回撥,而不是當前then執行後結果promise的失敗回撥,所以,可以將當前then的失敗回撥推到下一次的promise中,而丟擲異常就可以實現(因為上一個then丟擲異常,可以傳遞到下一個then的失敗回撥中
// catch方法實現
catch(onRejected) {
  return this.then(undefined, onRejected)
}

then方法改進:

簡單測試一下:

const p = new MyPromise((resolve, reject) => {
  reject('err message')
})

p.then(res => {
  console.log(res)
}).catch(err => {
  console.log(err) // err message
})

7.4.finally方法的實現

  • finally方法不管是Promise狀態變成fulfilled還是rejected都會被執行;
  • 這裡可以巧妙的藉助then方法,不管then是執行成功的回撥還是失敗的回撥,都去執行finally中的回撥即可;
  • 注意:如果在finally之前使用了catch,因為catch的實現也是去呼叫then,並且給then的成功回撥傳遞的是undefined,那麼執行到catch可能出現斷層的現象,導致不會執行到finally,也可以通過在then中新增判斷解決;
finally(onFinally) {
  this.then(() => {
    onFinally()
  }, () => {
    onFinally()
  })
  // 也可直接簡寫成:
  // this.then(onFinally, onFinally)
}

then方法改進:

簡單測試一下:

const p = new MyPromise((resolve, reject) => {
  resolve('aaaa')
})

p.then(res => {
  console.log('res:', res) // res: aaaa
}).catch(err => {
  console.log('err:', err)
}).finally(() => {
  console.log('我是一定會執行的!') // 我是一定會執行的!
})

7.5.resolve和reject方法的實現

  • resolve和reject類方法的實現就是去呼叫Promise中executor中的resolve和reject;
  • 注意:類方法需要加上static關鍵字;
static resolve(value) {
  return new MyPromise((resolve, reject) => resolve(value))
}

static reject(reasion) {
  return new MyPromise((resolve, reject) => reject(reasion))
}

簡單測試一下:

MyPromise.resolve('aaaa').then(res => {
  console.log(res) // aaaa
})
MyPromise.reject('bbbb').then(res => {
  console.log(res)
}, err => {
  console.log(err) // bbbb
})

7.6.all方法的實現

  • all方法可接收一個promise陣列,當所有promise狀態都變為fulfilled,就返回所有promise成功的回撥值(一個陣列),當其中有一個promise狀態變為了rejected,就返回該promise的狀態;
  • all實現的關鍵:當所有promise狀態變為fulfilled就去呼叫resolve,當有一個promise狀態變為rejected就去呼叫reject
static all(promises) {
  return new MyPromise((resolve, reject) => {
    // 用於存放所有成功的返回值
    const results = []
    promises.forEach(promise => {
      promise.then(res => {
        results.push(res)

        // 當成功返回值的長度與傳入promises的長度相等,就呼叫resolve
        if (results.length === promises.length) {
          resolve(results)
        }
      }, err => {
        // 一旦有一個promise變成了rejected狀態,就呼叫reject
        reject(err)
      })
    })
  })
}

簡單測試一下:

const p1 = new MyPromise(resolve => {
  setTimeout(() => {
    resolve('aaaa')
  }, 1000)
})
const p2 = new MyPromise(resolve => {
  setTimeout(() => {
    resolve('bbbb')
  }, 2000)
})
const p3 = new MyPromise(resolve => {
  setTimeout(() => {
    resolve('cccc')
  }, 3000)
})

MyPromise.all([p1, p2, p3]).then(res => {
  console.log(res) // [ 'aaaa', 'bbbb', 'cccc' ]
}).catch(err => {
  console.log(err)
})

7.7.allSettled方法的實現

  • allSettled方法會返回所有promise的結果陣列,陣列中包含每一個promise的狀態和值;
  • 不管promise的狀態為什麼,最終都會呼叫resolve;
static allSettled(promises) {
  return new MyPromise((resolve, reject) => {
    // 用於存放所有promise的狀態和返回值
    const results = []
    promises.forEach(promise => {
      promise.then(res => {
        results.push({ status: FULFILLED_STATUS, value: res })
        // 當長度相等,呼叫resolve
        if (results.length === promises.length) {
          resolve(results)
        }
      }, err => {
        results.push({ status: REJECTED_STATUS, value: err })
        // 當長度相等,呼叫resolve
        if (results.length === promises.length) {
          resolve(results)
        }
      })
    })
  })
}

簡單測試一下:

const p1 = new MyPromise(resolve => {
  setTimeout(() => {
    resolve('aaaa')
  }, 1000)
})
const p2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('err message')
  }, 2000)
})
const p3 = new MyPromise(resolve => {
  setTimeout(() => {
    resolve('bbbb')
  }, 3000)
})

MyPromise.allSettled([p1, p2, p3]).then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})

7.8.race方法的實現

  • race方法是獲取最先改變狀態的Promise,並以該Promise的狀態作為自己的狀態;
static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(promise => {
      // 得到狀態最先改變的promise,呼叫對應的resolve和reject
      promise.then(res => {
        resolve(resolve)
      }, err => {
        reject(err)
      })
    })
  })
}

7.9.any方法的實現

  • any方法會等到有一個Promise的狀態變成fulfilled,最終就是fulfilled狀態;
  • 如果傳入的所有Promise都為rejected狀態,會返回一個AggregateError,並且可以在AggregateError中的errors屬性中獲取所有錯誤資訊;
static any(promises) {
  return new MyPromise((resolve, reject) => {
    // 用於記錄狀態為rejected的值
    const reasons = []
    promises.forEach(promise => {
      promise.then(res => {
        // 當有一個promise變成fulfilled狀態就呼叫resolve
        resolve(res)
      }, err => {
        reasons.push(err)
        // 當所有promise都是rejected就呼叫reject,並且傳入AggregateError
        if (reasons.length === promises.length) {
          reject(new AggregateError(reasons))
        }
      })
    })
  })
}

簡單測試一下:

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('err message1')
  }, 1000)
})
const p2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('err message2')
  }, 2000)
})
const p3 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('err message3')
  }, 3000)
})

MyPromise.any([p1, p2, p3]).then(res => {
  console.log(res)
}).catch(err => {
  console.log('err:', err)
  console.log(err.errors)
})

7.10.Promise手寫完整版整理

上面已經對Promise的各個功能進行了實現,下面就來整理一下最終的完整版,可以將一些重複的邏輯抽取出去,比如try catch。

// 定義Promise的三種狀態常量
const PENDING_STATUS = 'pending'
const FULFILLED_STATUS = 'fulfilled'
const REJECTED_STATUS = 'rejected'
// try catch邏輯抽取
function tryCatchFn(execFn, value, resolve, reject) {
  try {
    const result = execFn(value)
    resolve(result)
  } catch (err) {
    reject(err)
  }
}
class MyPromise {
  constructor(executor) {
    // 初始化Promise的狀態為pending
    this.promiseStatus = PENDING_STATUS
    // 初始化變數,用於儲存resolve和reject傳入的引數值
    this.value = undefined
    this.reason = undefined
    // 初始化兩個陣列,分別用於儲存then中對應需要執行的回撥
    this.onFulfilledFns = []
    this.onRejectedFns = []

    // 1.定義executor需要傳入的resolve函式
    const resolve = (value) => {
      // 只有當Promise的狀態為pending,才能將狀態改變fulfilled
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = FULFILLED_STATUS
          this.value = value
          // 狀態變成fulfilled就去遍歷呼叫onFulfilled
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        })
      }
    }

    // 2.定義executor需要傳入的reject函式
    const reject = (reason) => {
      // 只有當Promise的狀態為pending,才能將狀態改變為rejected
      if (this.promiseStatus === PENDING_STATUS) {
        // 新增微任務
        queueMicrotask(() => {
          // 如果Promise狀態不為pending,後面的程式碼就不再執行了
          if (this.promiseStatus !== PENDING_STATUS) return
          this.promiseStatus = REJECTED_STATUS
          this.reason = reason
          // 狀態變成rejected就去遍歷呼叫onRejected
          this.onRejectedFns.forEach(fn => {
            fn(this.reason)
          })
        })
      }
    }

    // 3.將定義的兩個函式傳入executor並呼叫
    // 如果executor中就丟擲了異常,那麼直接執行reject即可
    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    // 判斷onRejected是否有值,沒有值的話直接賦值一個丟擲異常的方法,用於傳遞到下一次then中的失敗回撥,供catch呼叫
    onRejected = onRejected || (err => {throw err})

    // 判斷onFulfilled是否有值,避免在使用catch時傳入的undefined不會執行,出現斷層現象
    onFulfilled = onFulfilled || (value => value)

    return new MyPromise((resolve, reject) => {
      // 1.如果在呼叫then時,Promise的狀態已經確定了,就直接執行回撥
      if (this.promiseStatus === FULFILLED_STATUS) {
        // 通過try catch捕獲異常,沒有捕獲到執行resolve,捕獲到執行reject
        tryCatchFn(onFulfilled, this.value, resolve, reject)
      }
      if (this.promiseStatus === REJECTED_STATUS) {
        tryCatchFn(onRejected, this.reason, resolve, reject)
      }

      // 2.如果呼叫then時,Promise的狀態還沒確定下來,就放入微任務中執行
      if (this.promiseStatus === PENDING_STATUS) {
        // 將then中成功的回撥和失敗的回撥分別存入陣列中
        // 將傳入的回撥外包裹一層函式,目的是為了這裡能夠拿到then中回撥執行的結果
        this.onFulfilledFns.push(() => {
          tryCatchFn(onFulfilled, this.value, resolve, reject)
        })
        this.onRejectedFns.push(() => {
          tryCatchFn(onRejected, this.reason, resolve, reject)
        })
      }
    })
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }

  finally(onFinally) {
    this.then(onFinally, onFinally)
  }

  static resolve(value) {
    return new MyPromise((resolve, reject) => resolve(value))
  }

  static reject(reasion) {
    return new MyPromise((resolve, reject) => reject(reasion))
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      // 用於存放所有成功的返回值
      const results = []
      promises.forEach(promise => {
        promise.then(res => {
          results.push(res)

          // 當成功返回值的長度與傳入promises的長度相等,就呼叫resolve
          if (results.length === promises.length) {
            resolve(results)
          }
        }, err => {
          // 一旦有一個promise變成了rejected狀態,就呼叫reject
          reject(err)
        })
      })
    })
  }

  static allSettled(promises) {
    return new MyPromise((resolve, reject) => {
      // 用於存放所有promise的狀態和返回值
      const results = []
      promises.forEach(promise => {
        promise.then(res => {
          results.push({ status: FULFILLED_STATUS, value: res })
          // 當長度相等,呼叫resolve
          if (results.length === promises.length) {
            resolve(results)
          }
        }, err => {
          results.push({ status: REJECTED_STATUS, value: err })
          // 當長度相等,呼叫resolve
          if (results.length === promises.length) {
            resolve(results)
          }
        })
      })
    })
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(promise => {
        // 得到狀態最先改變的promise,呼叫對應的resolve和reject
        promise.then(resolve, reject)
      })
    })
  }

  static any(promises) {
    return new MyPromise((resolve, reject) => {
      // 用於記錄狀態為rejected的值
      const reasons = []
      promises.forEach(promise => {
        promise.then(res => {
          // 當有一個promise變成fulfilled狀態就呼叫resolve
          resolve(res)
        }, err => {
          reasons.push(err)
          // 當所有promise都是rejected就呼叫reject,並且傳入AggregateError
          if (reasons.length === promises.length) {
            reject(new AggregateError(reasons))
          }
        })
      })
    })
  }
}

相關文章