手把手教你實(shí)現(xiàn) Promise的使用方法
前言
很多 JavaScript 的初學(xué)者都曾感受過被回調(diào)地獄支配的恐懼,直至掌握了 Promise 語法才算解脫。雖然很多語言都早已內(nèi)置了 Promise ,但是 JavaScript 中真正將其發(fā)揚(yáng)光大的還是 jQuery 1.5 對(duì) $.ajax
的重構(gòu),支持了 Promise,而且用法也和 jQuery 推崇的鏈?zhǔn)秸{(diào)用不謀而合。后來 ES6 出世,大家才開始進(jìn)入全民 Promise 的時(shí)代,再后來 ES8 又引入了 async 語法,讓 JavaScript 的異步寫法更加優(yōu)雅。
今天我們就一步一步來實(shí)現(xiàn)一個(gè) Promise,如果你還沒有用過 Promise,建議先熟悉一下 Promise 語法再來閱讀本文。
構(gòu)造函數(shù)
在已有的 Promise/A+ 規(guī)范 中并沒有規(guī)定 promise 對(duì)象從何而來,在 jQuery 中通過調(diào)用 $.Deferred()
得到 promise 對(duì)象,ES6 中通過實(shí)例化 Promise 類得到 promise 對(duì)象。這里我們使用 ES 的語法,構(gòu)造一個(gè)類,通過實(shí)例化的方式返回 promise 對(duì)象,由于 Promise 已經(jīng)存在,我們暫時(shí)給這個(gè)類取名為 Deferred
。
class Deferred { constructor(callback) { const resolve = () => { // TODO } const reject = () => { // TODO } try { callback(resolve, reject) } catch (error) { reject(error) } } }
構(gòu)造函數(shù)接受一個(gè) callback,調(diào)用 callback 的時(shí)候需傳入 resolve、reject 兩個(gè)方法。
Promise 的狀態(tài)
Promise 一共分為三個(gè)狀態(tài):
pending
:等待中,這是 Promise 的初始狀態(tài);
fulfilled
:已結(jié)束,正常調(diào)用 resolve 的狀態(tài);
rejected
:已拒絕,內(nèi)部出現(xiàn)錯(cuò)誤,或者是調(diào)用 reject 之后的狀態(tài);
我們可以看到 Promise 在運(yùn)行期間有一個(gè)狀態(tài),存儲(chǔ)在 [[PromiseState]]
中。下面我們?yōu)?Deferred 添加一個(gè)狀態(tài)。
//基礎(chǔ)變量的定義 const STATUS = { PENDING: 'PENDING', FULFILLED: 'FULFILLED', REJECTED: 'REJECTED' } class Deferred { constructor(callback) { this.status = STATUS.PENDING const resolve = () => { // TODO } const reject = () => { // TODO } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
這里還有個(gè)有意思的事情,早期瀏覽器的實(shí)現(xiàn)中 fulfilled 狀態(tài)是 resolved,明顯與 Promise 規(guī)范不符。當(dāng)然,現(xiàn)在已經(jīng)修復(fù)了。
內(nèi)部結(jié)果
除開狀態(tài),Promise 內(nèi)部還有個(gè)結(jié)果 [[PromiseResult]]
,用來暫存 resolve/reject 接受的值。
繼續(xù)在構(gòu)造函數(shù)中添加一個(gè)內(nèi)部結(jié)果。
class Deferred { constructor(callback) { this.value = undefined this.status = STATUS.PENDING const resolve = value => { this.value = value // TODO } const reject = reason => { this.value = reason // TODO } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
儲(chǔ)存回調(diào)
使用 Promise 的時(shí)候,我們一般都會(huì)調(diào)用 promise 對(duì)象的 .then
方法,在 promise 狀態(tài)轉(zhuǎn)為 fulfilled
或 rejected
的時(shí)候,拿到內(nèi)部結(jié)果,然后做后續(xù)的處理。所以構(gòu)造函數(shù)中,還需要構(gòu)造兩個(gè)數(shù)組,用來存儲(chǔ) .then
方法傳入的回調(diào)。
class Deferred { constructor(callback) { this.value = undefined this.status = STATUS.PENDING this.rejectQueue = [] this.resolveQueue = [] const resolve = value => { this.value = value // TODO } const reject = reason => { this.value = reason // TODO } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
resolve 與 reject
修改狀態(tài)
接下來,我們需要實(shí)現(xiàn) resolve 和 reject 兩個(gè)方法,這兩個(gè)方法在被調(diào)用的時(shí)候,會(huì)改變 promise 對(duì)象的狀態(tài)。而且任意一個(gè)方法在被調(diào)用之后,另外的方法是無法被調(diào)用的。
new Promise((resolve, reject) => { setTimeout(() => { resolve('🙆♂️') }, 500) setTimeout(() => { reject('🙅♂️') }, 800) }).then( () => { console.log('fulfilled') }, () => { console.log('rejected') } )
此時(shí),控制臺(tái)只會(huì)打印出 fulfilled
,并不會(huì)出現(xiàn) rejected
。
class Deferred { constructor(callback) { this.value = undefined this.status = STATUS.PENDING this.rejectQueue = [] this.resolveQueue = [] let called // 用于判斷狀態(tài)是否被修改 const resolve = value => { if (called) return called = true this.value = value // 修改狀態(tài) this.status = STATUS.FULFILLED } const reject = reason => { if (called) return called = true this.value = reason // 修改狀態(tài) this.status = STATUS.REJECTED } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
調(diào)用回調(diào)
修改完?duì)顟B(tài)后,拿到結(jié)果的 promise 一般會(huì)調(diào)用 then 方法傳入的回調(diào)。
class Deferred { constructor(callback) { this.value = undefined this.status = STATUS.PENDING this.rejectQueue = [] this.resolveQueue = [] let called // 用于判斷狀態(tài)是否被修改 const resolve = value => { if (called) return called = true this.value = value // 修改狀態(tài) this.status = STATUS.FULFILLED // 調(diào)用回調(diào) for (const fn of this.resolveQueue) { fn(this.value) } } const reject = reason => { if (called) return called = true this.value = reason // 修改狀態(tài) this.status = STATUS.REJECTED // 調(diào)用回調(diào) for (const fn of this.rejectQueue) { fn(this.value) } } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
熟悉 JavaScript 事件系統(tǒng)的同學(xué)應(yīng)該知道, promise.then
方法中的回調(diào)會(huì)被放置到微任務(wù)隊(duì)列中,然后異步調(diào)用。
所以,我們需要將回調(diào)的調(diào)用放入異步隊(duì)列,這里我們可以放到 setTimeout 中進(jìn)行延遲調(diào)用,雖然不太符合規(guī)范,但是將就將就。
class Deferred { constructor(callback) { this.value = undefined this.status = STATUS.PENDING this.rejectQueue = [] this.resolveQueue = [] let called // 用于判斷狀態(tài)是否被修改 const resolve = value => { if (called) return called = true // 異步調(diào)用 setTimeout(() => { this.value = value // 修改狀態(tài) this.status = STATUS.FULFILLED // 調(diào)用回調(diào) for (const fn of this.resolveQueue) { fn(this.value) } }) } const reject = reason => { if (called) return called = true // 異步調(diào)用 setTimeout(() =>{ this.value = reason // 修改狀態(tài) this.status = STATUS.REJECTED // 調(diào)用回調(diào) for (const fn of this.rejectQueue) { fn(this.value) } }) } try { callback(resolve, reject) } catch (error) { // 出現(xiàn)異常直接進(jìn)行 reject reject(error) } } }
then 方法
接下來我們需要實(shí)現(xiàn) then 方法,用過 Promise 的同學(xué)肯定知道,then 方法是能夠繼續(xù)進(jìn)行鏈?zhǔn)秸{(diào)用的,所以 then 必須要返回一個(gè) promise 對(duì)象。但是在 Promise/A+
規(guī)范中,有明確的規(guī)定,then 方法返回的是一個(gè)新的 promise 對(duì)象,而不是直接返回 this,這一點(diǎn)我們可以通過下面代碼驗(yàn)證一下。
可以看到 p1
對(duì)象和 p2
是兩個(gè)不同的對(duì)象,并且 then 方法返回的 p2
對(duì)象也是 Promise 的實(shí)例。
除此之外,then 方法還需要判斷當(dāng)前狀態(tài),如果當(dāng)前狀態(tài)不是 pending
狀態(tài),則可以直接調(diào)用傳入的回調(diào),而不用再放入隊(duì)列進(jìn)行等待。
class Deferred { then(onResolve, onReject) { if (this.status === STATUS.PENDING) { // 將回調(diào)放入隊(duì)列中 const rejectQueue = this.rejectQueue const resolveQueue = this.resolveQueue return new Deferred((resolve, reject) => { // 暫存到成功回調(diào)等待調(diào)用 resolveQueue.push(function (innerValue) { try { const value = onResolve(innerValue) // 改變當(dāng)前 promise 的狀態(tài) resolve(value) } catch (error) { reject(error) } }) // 暫存到失敗回調(diào)等待調(diào)用 rejectQueue.push(function (innerValue) { try { const value = onReject(innerValue) // 改變當(dāng)前 promise 的狀態(tài) resolve(value) } catch (error) { reject(error) } }) }) } else { const innerValue = this.value const isFulfilled = this.status === STATUS.FULFILLED return new Deferred((resolve, reject) => { try { const value = isFulfilled ? onResolve(innerValue) // 成功狀態(tài)調(diào)用 onResolve : onReject(innerValue) // 失敗狀態(tài)調(diào)用 onReject resolve(value) // 返回結(jié)果給后面的 then } catch (error) { reject(error) } }) } } }
現(xiàn)在我們的邏輯已經(jīng)可以基本跑通,我們先試運(yùn)行一段代碼:
new Deferred(resolve => { setTimeout(() => { resolve(1) }, 3000) }).then(val1 => { console.log('val1', val1) return val1 * 2 }).then(val2 => { console.log('val2', val2) return val2 })
3 秒后,控制臺(tái)出現(xiàn)如下結(jié)果:
可以看到,這基本符合我們的預(yù)期。
值穿透
如果我們?cè)谡{(diào)用 then 的時(shí)候,如果沒有傳入任何的參數(shù),按照規(guī)范,當(dāng)前 promise 的值是可以透?jìng)鞯较乱粋€(gè) then 方法的。例如,如下代碼:
new Deferred(resolve => { resolve(1) }) .then() .then() .then(val => { console.log(val) })
在控制臺(tái)并沒有看到任何輸出,而切換到 Promise 是可以看到正確結(jié)果的。
要解決這個(gè)方法很簡(jiǎn)單,只需要在 then 調(diào)用的時(shí)候判斷參數(shù)是否為一個(gè)函數(shù),如果不是則需要給一個(gè)默認(rèn)值。
const isFunction = fn => typeof fn === 'function' class Deferred { then(onResolve, onReject) { // 解決值穿透 onReject = isFunction(onReject) ? onReject : reason => { throw reason } onResolve = isFunction(onResolve) ? onResolve : value => { return value } if (this.status === STATUS.PENDING) { // ... } else { // ... } } }
現(xiàn)在我們已經(jīng)可以拿到正確結(jié)果了。
一步之遙
現(xiàn)在我們距離完美實(shí)現(xiàn) then 方法只差一步之遙,那就是我們?cè)谡{(diào)用 then 方法傳入的 onResolve/onReject
回調(diào)時(shí),還需要判斷他們的返回值。如果回調(diào)的內(nèi)部返回的就是一個(gè) promise 對(duì)象,我們應(yīng)該如何處理?或者出現(xiàn)了循環(huán)引用,我們又該怎么處理?
前面我們?cè)谀玫?onResolve/onReject
的返回值后,直接就調(diào)用了 resolve
或者 resolve
,現(xiàn)在我們需要把他們的返回值進(jìn)行一些處理。
then(onResolve, onReject) { // 解決值穿透代碼已經(jīng)省略 if (this.status === STATUS.PENDING) { // 將回調(diào)放入隊(duì)列中 const rejectQueue = this.rejectQueue const resolveQueue = this.resolveQueue const promise = new Deferred((resolve, reject) => { // 暫存到成功回調(diào)等待調(diào)用 resolveQueue.push(function (innerValue) { try { const value = onResolve(innerValue) - resolve(value) + doThenFunc(promise, value, resolve, reject) } catch (error) { reject(error) } }) // 暫存到失敗回調(diào)等待調(diào)用 rejectQueue.push(function (innerValue) { try { const value = onReject(innerValue) - resolve(value) + doThenFunc(promise, value, resolve, reject) } catch (error) { reject(error) } }) }) return promise } else { const innerValue = this.value const isFulfilled = this.status === STATUS.FULFILLED const promise = new Deferred((resolve, reject) => { try { const value = isFulfilled ? onResolve(innerValue) // 成功狀態(tài)調(diào)用 onResolve : onReject(innerValue) // 失敗狀態(tài)調(diào)用 onReject - resolve(value) + doThenFunc(promise, value, resolve, reject) } catch (error) { reject(error) } }) return promise } }
返回值判斷
在我們使用 Promise 的時(shí)候,經(jīng)常會(huì)在 then 方法中返回一個(gè)新的 Promise,然后把新的 Promise 完成后的內(nèi)部結(jié)果再傳遞給后面的 then 方法。
fetch('server/login') .then(user => { // 返回新的 promise 對(duì)象 return fetch(`server/order/${user.id}`) }) .then(order => { console.log(order) })
function doThenFunc(promise, value, resolve, reject) { // 如果 value 是 promise 對(duì)象 if (value instanceof Deferred) { // 調(diào)用 then 方法,等待結(jié)果 value.then( function (val) { doThenFunc(promise, value, resolve, reject) }, function (reason) { reject(reason) } ) return } // 如果非 promise 對(duì)象,則直接返回 resolve(value) }
判斷循環(huán)引用
如果當(dāng)前 then 方法回調(diào)函數(shù)返回值是當(dāng)前 then 方法產(chǎn)生的新的 promise 對(duì)象,則被認(rèn)為是循環(huán)引用,具體案例如下:
then 方法返回的新的 promise 對(duì)象 p1
,在回調(diào)中被當(dāng)做返回值,此時(shí)會(huì)拋出一個(gè)異常。因?yàn)榘凑罩暗倪壿?,代碼將會(huì)一直困在這一段邏輯里。
所以,我們需要提前預(yù)防,及時(shí)拋出錯(cuò)誤。
function doThenFunc(promise, value, resolve, reject) { // 循環(huán)引用 if (promise === value) { reject( new TypeError('Chaining cycle detected for promise') ) return } // 如果 value 是 promise 對(duì)象 if (value instanceof Deferred) { // 調(diào)用 then 方法,等待結(jié)果 value.then( function (val) { doThenFunc(promise, value, resolve, reject) }, function (reason) { reject(reason) } ) return } // 如果非 promise 對(duì)象,則直接返回 resolve(value) }
現(xiàn)在我們?cè)僭囋囋?then 中返回一個(gè)新的 promise 對(duì)象。
const delayDouble = (num, time) => new Deferred((resolve) => { console.log(new Date()) setTimeout(() => { resolve(2 * num) }, time) }) new Deferred(resolve => { setTimeout(() => { resolve(1) }, 2000) }) .then(val => { console.log(new Date(), val) return delayDouble(val, 2000) }) .then(val => { console.log(new Date(), val) })
上面的結(jié)果也是完美符合我們的預(yù)期。
catch 方法
catch 方法其實(shí)很簡(jiǎn)單,相當(dāng)于 then 方法的一個(gè)簡(jiǎn)寫。
class Deferred { constructor(callback) {} then(onResolve, onReject) {} catch(onReject) { return this.then(null, onReject) } }
靜態(tài)方法
resolve/reject
Promise 類還提供了兩個(gè)靜態(tài)方法,直接返回狀態(tài)已經(jīng)固定的 promise 對(duì)象。
class Deferred { constructor(callback) {} then(onResolve, onReject) {} catch(onReject) {} static resolve(value) { return new Deferred((resolve, reject) => { resolve(value) }) } static reject(reason) { return new Deferred((resolve, reject) => { reject(reason) }) } }
all
all 方法接受一個(gè) promise 對(duì)象的數(shù)組,等數(shù)組中所有的 promise 對(duì)象的狀態(tài)變?yōu)?fulfilled
,然后返回結(jié)果,其結(jié)果也是一個(gè)數(shù)組,數(shù)組的每個(gè)值對(duì)應(yīng)的是 promise 對(duì)象的內(nèi)部結(jié)果。
首先,我們需要先判斷傳入的參數(shù)是否為數(shù)組,然后構(gòu)造一個(gè)結(jié)果數(shù)組以及一個(gè)新的 promise 對(duì)象。
class Deferred { static all(promises) { // 非數(shù)組參數(shù),拋出異常 if (!Array.isArray(promises)) { return Deferred.reject(new TypeError('args must be an array')) } // 用于存儲(chǔ)每個(gè) promise 對(duì)象的結(jié)果 const result = [] const length = promises.length // 如果 remaining 歸零,表示所有 promise 對(duì)象已經(jīng) fulfilled let remaining = length const promise = new Deferred(function (resolve, reject) { // TODO }) return promise } }
接下來,我們需要進(jìn)行一下判斷,對(duì)每個(gè) promise 對(duì)象的 resolve 進(jìn)行攔截,每次 resolve 都需要將 remaining
減一,直到 remaining
歸零。
class Deferred { static all(promises) { // 非數(shù)組參數(shù),拋出異常 if (!Array.isArray(promises)) { return Deferred.reject(new TypeError('args must be an array')) } const result = [] // 用于存儲(chǔ)每個(gè) promise 對(duì)象的結(jié)果 const length = promises.length let remaining = length const promise = new Deferred(function (resolve, reject) { // 如果數(shù)組為空,則返回空結(jié)果 if (promises.length === 0) return resolve(result) function done(index, value) { doThenFunc( promise, value, (val) => { // resolve 的結(jié)果放入 result 中 result[index] = val if (--remaining === 0) { // 如果所有的 promise 都已經(jīng)返回結(jié)果 // 然后運(yùn)行后面的邏輯 resolve(result) } }, reject ) } // 放入異步隊(duì)列 setTimeout(() => { for (let i = 0; i < length; i++) { done(i, promises[i]) } }) }) return promise } }
下面我們通過如下代碼,判斷邏輯是否正確。按照預(yù)期,代碼運(yùn)行后,在 3 秒之后,控制臺(tái)會(huì)打印一個(gè)數(shù)組 [2, 4, 6]
。
const delayDouble = (num, time) => new Deferred((resolve) => { setTimeout(() => { resolve(2 * num) }, time) }) console.log(new Date()) Deferred.all([ delayDouble(1, 1000), delayDouble(2, 2000), delayDouble(3, 3000) ]).then((results) => { console.log(new Date(), results) })
上面的運(yùn)行結(jié)果,基本符合我們的預(yù)期。
race
race 方法同樣接受一個(gè) promise 對(duì)象的數(shù)組,但是它只需要有一個(gè) promise 變?yōu)?fulfilled
狀態(tài)就會(huì)返回結(jié)果。
class Deferred { static race(promises) { if (!Array.isArray(promises)) { return Deferred.reject(new TypeError('args must be an array')) } const length = promises.length const promise = new Deferred(function (resolve, reject) { if (promises.length === 0) return resolve([]) function done(value) { doThenFunc(promise, value, resolve, reject) } // 放入異步隊(duì)列 setTimeout(() => { for (let i = 0; i < length; i++) { done(promises[i]) } }) }) return promise } }
下面我們將前面驗(yàn)證 all 方法的案例改成 race。按照預(yù)期,代碼運(yùn)行后,在 1 秒之后,控制臺(tái)會(huì)打印一個(gè)2。
const delayDouble = (num, time) => new Deferred((resolve) => { setTimeout(() => { resolve(2 * num) }, time) }) console.log(new Date()) Deferred.race([ delayDouble(1, 1000), delayDouble(2, 2000), delayDouble(3, 3000) ]).then((results) => { console.log(new Date(), results) })
上面的運(yùn)行結(jié)果,基本符合我們的預(yù)期。
總結(jié)
一個(gè)簡(jiǎn)易版的 Promise 類就已經(jīng)實(shí)現(xiàn)了,這里還是省略了部分細(xì)節(jié),完整代碼可以訪問 github 。Promise 的出現(xiàn)為后期的 async 語法打下了堅(jiān)實(shí)基礎(chǔ),下一篇博客可以好好聊一聊 JavaScript 的異步編程史,不小心又給自己挖坑了。。。
到此這篇關(guān)于手把手教你實(shí)現(xiàn) Promise的方法的文章就介紹到這了,更多相關(guān)Promise語法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何用Node.js編寫內(nèi)存效率高的應(yīng)用程序
這篇文章主要介紹了如何用Node.js編寫內(nèi)存效率高的應(yīng)用程序,對(duì)Node.js感興趣的同學(xué),可以參考下2021-04-04Node.js使用bcrypt-pbkdf實(shí)現(xiàn)密碼加密
在這個(gè)數(shù)字時(shí)代,保護(hù)用戶密碼的重要性不言而喻,作為一名資深的前端開發(fā)工程師和技術(shù)博客作者,今天我將帶你詳細(xì)了解如何在 Node.js 環(huán)境中利用 bcrypt-pbkdf 模塊進(jìn)行密碼的哈希處理,確保你的應(yīng)用安全性得到有效提升,需要的朋友可以參考下2024-05-05nodejs npm install全局安裝和本地安裝的區(qū)別
這篇文章主要介紹了nodejs npm install 全局安裝和非全局安裝的區(qū)別,即帶參數(shù)-g和不帶參數(shù)-g安裝的區(qū)別,需要的朋友可以參考下2014-06-06Node.js實(shí)戰(zhàn)之Buffer和Stream模塊系統(tǒng)深入剖析詳解
這篇文章主要介紹了Node.js實(shí)戰(zhàn)之Buffer和Stream模塊系統(tǒng)深入剖析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08手把手教你把nodejs部署到linux上跑出hello world
本篇文章主要介紹了手把手教你把nodejs部署到linux上跑出hello world,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-06-06nodejs獲取本機(jī)內(nèi)網(wǎng)和外網(wǎng)ip地址的實(shí)現(xiàn)代碼
這篇文章主要介紹了nodejs獲取本機(jī)內(nèi)網(wǎng)和外網(wǎng)ip地址的實(shí)現(xiàn)代碼,需要的朋友可以參考下2014-06-06