最近這段時間因爲疫情的緣由,在家實在悶得慌,因此看了下 js 的一些基礎知識,從前不是很瞭解的 Promise 忽然豁然開朗了很多,因而就趕忙趁熱打鐵寫下來(這就是溫故而知新的感受嗎,哈哈哈😁)。git
確實是待久了,🌸櫻花🌸都開了。github
要想知道某個東西怎麼寫的,就要先學會用,因此讀者大大們若是還有沒用過的話,趕忙去學下再來看吧,由於本文的目標是手寫一個 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
下面的描述我也沒寫的規範那麼正經,由於我有時候總以爲那樣不利於理解,一堆英文名字就把你給愣住了😯。併發
首先咱們把前面提到的基礎用法簡化下:框架
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)
}
複製代碼
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 是有返回的,而且是個新的 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 執行完再返回,讓咱們看下下面的代碼:
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 差很少,可是規則有點不同,它也是接收一個數組,只不過返回的就一項,最早返回成功就成功,最早失敗就失敗,代碼也是和上面雷同,具體以下:
所謂最好的輸入就是輸出,一晃又是幾個月沒寫文章了,因此特此沉澱,仍是心虛啊。但願本篇文章可以對你有所幫助,不知道寫的清不清楚😁。最後祝福你們百毒不侵,回見👋。
ps:Gituhub 代碼地址