【朝花夕拾】手寫 Promise 你也能夠

前言

最近這段時間因爲疫情的緣由,在家實在悶得慌,因此看了下 js 的一些基礎知識,從前不是很瞭解的 Promise 忽然豁然開朗了很多,因而就趕忙趁熱打鐵寫下來(這就是溫故而知新的感受嗎,哈哈哈😁)。git

確實是待久了,🌸櫻花🌸都開了。github

爲何要用 Promise

  • 一個很顯然的緣由就是它的鏈式調用可以解決回調地獄帶來的一些問題(不利於閱讀與維護、很差調試等),這點想必你們都清楚。
  • 其實 Promise 和傳統的回調還有一個細微的區別,就是回調函數的定義時機,傳統的回調咱們是得先有回調函數再執行(由於你要把函數看成參數傳,得先寫好),而 Promise 則更加靈活,它是先執行函數體,而後在你須要 then 的時候再去寫回調函數也不遲,不知道說清楚沒有,你們能夠細品一下😂。

基礎用法

要想知道某個東西怎麼寫的,就要先學會用,因此讀者大大們若是還有沒用過的話,趕忙去學下再來看吧,由於本文的目標是手寫一個 Promise 以及 catch、finally、all、race、resolve 等附加方法,下面是最簡單的用法展現👇:數組

let p = new Promise((resolve, reject) => { // 這裏面的代碼是同步執行的
  resolve(1)
}).then(res => { // 這裏面的代碼是異步的
  console.log('成功', res)
}, err => {
  console.log('錯誤', err)
})
// 成功 1
複製代碼

知識前置

事實上 Promise 是有個 Promises/A+ 規範的(這東西至關於一個需求文檔),這裏我會先羅列幾個必知必會的點,畢竟要手寫嘛✍,肚子裏得有點墨水:promise

  • 咱們要知道一個 Promise 有三種狀態:pending,success,fail,而且狀態只能從 pending 到 success 或者從 pending 到 fail,而且只能改變一次,是不可逆的(正經點的狀態名稱:pending、fulfilled、rejected,固然這不重要)
  • resolve 和 reject 是用來改變 Promise 自身狀態和值的,並觸發後面 then 裏面的回調
  • then 裏面的參數若是不是函數將被忽略,具備穿透效果,這個後面會細說
  • new Promise() 返回一個 promise 對象,每一個 then 返回一個新的 Promise,由於 Promise 的狀態只容許被改變一次,因此每次都得返回新的

下面的描述我也沒寫的規範那麼正經,由於我有時候總以爲那樣不利於理解,一堆英文名字就把你給愣住了😯。併發

開始手寫

首先咱們把前面提到的基礎用法簡化下:框架

let p = new Promise(fn).then(fn1, fn2)
複製代碼

這樣一來上面的結構就明瞭許多,既然須要 new,那麼它首先是個構造函數,接收一個函數參數 fn,而後實例有個 then 方法,接收兩個參數 fn1 和 fn2,也都是函數。此外由知識前置裏面的內容可知 Promise 裏面自身得維護一個狀態,因此咱們能夠先寫出一個大致框架:dom

class MyPromise {
  constructor(fn) {
    this.status = 'pending' // 保存狀態
    this.successValue = null // 保存成功的值
    this.failValue = null // 保存失敗的值
    fn() // 由於 Promise 裏面的代碼是同步執行的,因此直接進來須要直接調用
  }
  then(successFn, failFn) {}
}
複製代碼

咱們知道在執行 fn 的時候其實還有兩個參數,就是 fn(resolve, reject),因此咱們須要完善它,在 MyPromise 裏面定義 resolve 和 reject 這兩個函數,當外界調用 resolve 和 reject 時就是改變 MyPromise 裏面的狀態和值,並觸發相應的回調函數,就像下面這樣:異步

constructor(fn) {
    this.status = 'pending' // 保存狀態
    this.successValue = null // 保存成功的值
    this.failValue = null // 保存失敗的值

    let resolve = (successValue) => { // 這個 successValue 是外部調用傳進來的值
      this.status = 'success'
      this.successValue = successValue
    }
    let reject = (failValue) => { // 這個 failValue 是外部調用傳進來的值
      this.status = 'fail'
      this.failValue = failValue
    }

    fn(resolve, reject)
}
複製代碼

then

constructor 裏面的東西大概寫完了,接下來咱們簡要寫下 then 方法,then 方法裏面不是有兩個函數參數嗎,根據 status 的狀態執行其中一個便可,就像下面這樣:函數

then(successFn, failFn) {
    if (this.status === 'success') {
        successFn(this.successValue)
    } else if (this.status === 'fail') {
        failFn(this.failValue)
    }
}
複製代碼

ok,咱們來測試一下:測試

let p = new MyPromise((resolve, reject) => {
  console.log('1')
  resolve(100)
}).then(res => {
  console.log('2')
  console.log('成功', res)
}, err => {
  console.log('錯誤', err)
})
console.log('3')
// 1
// 2
// 成功 100
// 3
複製代碼

不錯不錯,能夠成功打印出 100,可是有個問題,console.log 的順序應該是 一、三、2,由於 then 裏面的內容是異步執行的,因此咱們須要 setTimeout 來簡單模擬下,把 then 操做延後,就像下面這樣:

then(successFn, failFn) {
    if (this.status === 'success') {
      setTimeout(() => {
        successFn(this.successValue)
      })
    } else if (this.status === 'fail') {
      setTimeout(() => {
        failFn(this.failValue)
      })
    }
}
複製代碼

不錯不錯,看起來好像能夠了,可是若是我這樣寫呢:

let p = new MyPromise((resolve, reject) => { // 連續調用
    resolve(100)
    reject(-1)
}).then(res => {
  console.log('成功', res)
}, err => {
  console.log('錯誤', err)
})
// 錯誤 -1
複製代碼

上面結果輸出 -1 固然是錯的,由於連續調用 resolve 或 reject 是無效的,Promise 只容許被改變一次,因此咱們須要加個限制條件:

let resolve = (successValue) => {
  if (this.status !== 'pending') return // 狀態已經改變過就不往下執行了
  this.status = 'success'
  this.successValue = successValue
}
let reject = (failValue) => {
  if (this.status !== 'pending') return // 狀態已經改變過就不往下執行了
  this.status = 'fail'
  this.failValue = failValue
}
複製代碼

固然還沒完,一個新的問題誕生了,若是我這樣寫呢:

let p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(100)
  }, 2000);
}).then(res => {
  console.log(res)
}, err => {
  console.log(err)
})
複製代碼

沒有輸出任何東西,這是由於當我調用 then 的時候,MyPromise 裏面的東西還沒執行完,狀態未被改變,因此 then 裏面的成功和失敗都不會調用,所以咱們須要在 then 中加上一種狀況,就是若是 MyPromise 裏面還沒執行完就先把 then 中的 fn1 和 fn2 放進數組中存起來,等到 MyPromise 裏面執行完再把數組拿出來遍歷執行(固然也要放進 setTimeout 中),就像下面這樣(接下來都用圖了😁):

事實上你能夠把 resolve 或 reject 當成觸發 then 裏面函數的開關,只要碰到 resolve 或 reject,與之對應的 then 裏面的函數也該執行了(放入微任務執行,咱們用的是宏任務替代,關於事件循環也是個挺有意思的知識點,你們能夠本身去了解一下)。一句話說就是 resolve 和 reject 的做用就是在合適的時間點執行 then 裏面的回調函數,有點觀察者模式的意思(你品,你細品🤔)。

then 增強

寫到這裏,其實還有個大問題,前面說過了,then 是有返回的,而且是個新的 Promise,還支持鏈式調用,顯然咱們的並不具有這樣的功能,因此如今咱們須要完善 then,主要思想就是在 then 裏面套一層 Promise,此外你要知道若是 then(fn1, fn2) 繼續向下傳遞的話,傳遞的值是 fn1 或 fn2 的返回值,看下面這張圖你可能會清晰點:

👌,理清了這個東西以後咱們就來看下具體是怎麼改的:
細心點你會發現這裏 then 裏面函數的返回值還多是個 Promise,因此咱們須要對 then 裏面函數的返回值作個判斷,若是返回值是個 Promise 就須要等這個 Promise 執行完再調用 resolve 和 reject,就像下面這樣:
執行一下下面的測試代碼:

let p = new MyPromise((resolve, reject) => {
  resolve(100)
}).then(res => {
  console.log('成功', res)
  // return 0
  return new MyPromise((resolve2, reject2) => {
    setTimeout(() => {
      resolve2(0)
    }, 1000)
  })
}, err => {
  console.log('錯誤', err)
}).then(res2 => {
  console.log('成功2', res2)
}, err2 => {
  console.log('錯誤2', err2)
})
// 成功 100
// 成功2 0 (1s後打印出來)
複製代碼

寫到這裏,then 裏面的東西已經寫的差很少了,但其實仍是有問題的,咱們這裏只是解決了一層 Promise 的嵌套,若是你多嵌套幾個 Promise 就不行了,這個須要咱們把上面公共的部分提取出來而後遞歸調用,寫起來不復雜,但會有點繞容易暈,此外咱們也沒有對循環調用同一個 Promsie 作判斷以及一些異常捕獲,由於咱們理解到這裏就差很少了👏。固然了,我會在文末附上完整的代碼😬,裏面也有詳細的註釋。

穿透

什麼是穿透呢?讓咱們來看下面的代碼:

let p = new Promise((resolve, reject) => {
  resolve(100)
}).then()
.then(1)
.then(res => {
  console.log('成功', res)
}, err => {
  console.log('錯誤', err)
})
// 成功 100
複製代碼

簡單來講就是,若是 then 中的參數不是函數或爲空,then 以前的值還可以繼續向下傳遞,其實這個寫起來很簡單,就是在 then 裏面的一開始判斷下參數是否爲函數,不是的話就包裝成函數,並把以前的值看成返回值,具體操做以下:

注意咱們這裏拋出錯誤 throw 不能寫成這樣 return new Error('xxx'),由於這不是拋出錯誤,是返回一個錯誤對象,等價於 return {},它是個正常的返回值,而不是錯誤,好好體會一下。

catch

catch 這東西其實和 then 一毛同樣,只不過不須要成功回調,promise.catch(fn) 至關於 promise.then(null, fn),也就是說若是 catch 後面若是還有 then 也是能夠繼續執行的,咱們直接看下面的代碼就瞭解了:

Promise.resolve 和 Promise.reject

這裏咱們先簡要看一下 Promise.resolve 的用法:

Promise.resolve(1).then(res => console.log(res))
Promise.resolve(
    new Promise((resolve, reject) => resolve(2))
).then(res => console.log(res))
// 1
// 2
複製代碼

首先 Promise.resolve 是個靜態方法(就是隻能用類來調用,比如 Math.random()),它能夠進行鏈式調用,因此它返回的也是個 Promise,只不過要注意的是 resolve 裏面接收的參數能夠是 Promise 和通常值(數字、字符串、對象等),若是是 Promise 則須要等這個參數 Promise 執行完再返回,讓咱們看下下面的代碼:

Promise.reject 也是同樣的寫法,它們都算是語法糖,這裏就略過了。

finally

finally 的特色是不論正確與否,它總會執行,接收一個函數參數,而且返回 Promise,不過要注意的是它向下傳遞的值是上一次的值而不是 finally 中的值,具體以下:

Promise.all

仍是同樣咱們先來看下具體用法:

MyPromise.all([new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000);
}), 2, 3]).then(res => {
  console.log(res)
})
// [1, 2, 3] (1s後打印)
複製代碼

首先 all 方法接收一個數組參數,數組的每一項能夠是 Promise,也能夠是常量等有返回值的東西;其次使用 all 方法以後也能夠用 then 進行鏈式調用,因此 all 方法返回的也是個 Promise;最後 all 是併發執行,其實就是寫個循環,不過只有所有成功纔算是成功,不然就算失敗,直接看下面的代碼:

Promise.race

race 其實和上面的 all 差很少,可是規則有點不同,它也是接收一個數組,只不過返回的就一項,最早返回成功就成功,最早失敗就失敗,代碼也是和上面雷同,具體以下:

小結

所謂最好的輸入就是輸出,一晃又是幾個月沒寫文章了,因此特此沉澱,仍是心虛啊。但願本篇文章可以對你有所幫助,不知道寫的清不清楚😁。最後祝福你們百毒不侵,回見👋。

ps:Gituhub 代碼地址

相關文章
相關標籤/搜索