最近這段時間因爲疫情的緣由,在家實在悶得慌,因此看了下 js 的一些基礎知識,從前不是很瞭解的 Promise 忽然豁然開朗了很多,因而就趕忙趁熱打鐵寫下來(這就是溫故而知新的感受嗎,哈哈哈😁)。數組
確實是待久了,🌸櫻花🌸都開了。
要想知道某個東西怎麼寫的,就要先學會用,因此讀者大大們若是還有沒用過的話,趕忙去學下再來看吧,由於本文的目標是手寫一個 Promise 以及 catch、finally、all、race、resolve 等附加方法,下面是最簡單的用法展現👇:promise
let p = new Promise((resolve, reject) => { // 這裏面的代碼是同步執行的 resolve(1) }).then(res => { // 這裏面的代碼是異步的 console.log('成功', res) }, err => { console.log('錯誤', err) }) // 成功 1 複製代碼
事實上 Promise 是有個 Promises/A+ 規範的(這東西至關於一個需求文檔),這裏我會先羅列幾個必知必會的點,畢竟要手寫嘛✍,肚子裏得有點墨水:併發
下面的描述我也沒寫的規範那麼正經,由於我有時候總以爲那樣不利於理解,一堆英文名字就把你給愣住了😯。框架
首先咱們把前面提到的基礎用法簡化下:dom
let p = new Promise(fn).then(fn1, fn2) 複製代碼
這樣一來上面的結構就明瞭許多,既然須要 new,那麼它首先是個構造函數,接收一個函數參數 fn,而後實例有個 then 方法,接收兩個參數 fn1 和 fn2,也都是函數。此外由知識前置裏面的內容可知 Promise 裏面自身得維護一個狀態,因此咱們能夠先寫出一個大致框架:異步
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) } 複製代碼
constructor 裏面的東西大概寫完了,接下來咱們簡要寫下 then 方法,then 方法裏面不是有兩個函數參數嗎,根據 status 的狀態執行其中一個便可,就像下面這樣:測試
then(successFn, failFn) { if (this.status === 'success') { successFn(this.successValue) } else if (this.status === 'fail') { failFn(this.failValue) } } 複製代碼
ok,咱們來測試一下:this
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 操做延後,就像下面這樣:spa
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 是有返回的,而且是個新的 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 這東西其實和 then 一毛同樣,只不過不須要成功回調,promise.catch(fn) 至關於 promise.then(null, fn),也就是說若是 catch 後面若是還有 then 也是能夠繼續執行的,咱們直接看下面的代碼就瞭解了:
這裏咱們先簡要看一下 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 的特色是不論正確與否,它總會執行,接收一個函數參數,而且返回 Promise,不過要注意的是它向下傳遞的值是上一次的值而不是 finally 中的值,具體以下:
仍是同樣咱們先來看下具體用法:
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 是併發執行,其實就是寫個循環,不過只有所有成功纔算是成功,不然就算失敗,直接看下面的代碼:
race 其實和上面的 all 差很少,可是規則有點不同,它也是接收一個數組,只不過返回的就一項,最早返回成功就成功,最早失敗就失敗,代碼也是和上面雷同,具體以下: