手把手教你實現(xiàn) Promise的使用方法
前言
很多 JavaScript 的初學者都曾感受過被回調(diào)地獄支配的恐懼,直至掌握了 Promise 語法才算解脫。雖然很多語言都早已內(nèi)置了 Promise ,但是 JavaScript 中真正將其發(fā)揚光大的還是 jQuery 1.5 對 $.ajax 的重構(gòu),支持了 Promise,而且用法也和 jQuery 推崇的鏈式調(diào)用不謀而合。后來 ES6 出世,大家才開始進入全民 Promise 的時代,再后來 ES8 又引入了 async 語法,讓 JavaScript 的異步寫法更加優(yōu)雅。
今天我們就一步一步來實現(xiàn)一個 Promise,如果你還沒有用過 Promise,建議先熟悉一下 Promise 語法再來閱讀本文。
構(gòu)造函數(shù)
在已有的 Promise/A+ 規(guī)范 中并沒有規(guī)定 promise 對象從何而來,在 jQuery 中通過調(diào)用 $.Deferred() 得到 promise 對象,ES6 中通過實例化 Promise 類得到 promise 對象。這里我們使用 ES 的語法,構(gòu)造一個類,通過實例化的方式返回 promise 對象,由于 Promise 已經(jīng)存在,我們暫時給這個類取名為 Deferred 。
class Deferred {
constructor(callback) {
const resolve = () => {
// TODO
}
const reject = () => {
// TODO
}
try {
callback(resolve, reject)
} catch (error) {
reject(error)
}
}
}
構(gòu)造函數(shù)接受一個 callback,調(diào)用 callback 的時候需傳入 resolve、reject 兩個方法。
Promise 的狀態(tài)
Promise 一共分為三個狀態(tài):

pending :等待中,這是 Promise 的初始狀態(tài);
fulfilled :已結(jié)束,正常調(diào)用 resolve 的狀態(tài);
rejected :已拒絕,內(nèi)部出現(xiàn)錯誤,或者是調(diào)用 reject 之后的狀態(tài);

我們可以看到 Promise 在運行期間有一個狀態(tài),存儲在 [[PromiseState]] 中。下面我們?yōu)?Deferred 添加一個狀態(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)異常直接進行 reject
reject(error)
}
}
}
這里還有個有意思的事情,早期瀏覽器的實現(xiàn)中 fulfilled 狀態(tài)是 resolved,明顯與 Promise 規(guī)范不符。當然,現(xiàn)在已經(jīng)修復了。

內(nèi)部結(jié)果
除開狀態(tài),Promise 內(nèi)部還有個結(jié)果 [[PromiseResult]] ,用來暫存 resolve/reject 接受的值。


繼續(xù)在構(gòu)造函數(shù)中添加一個內(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)異常直接進行 reject
reject(error)
}
}
}
儲存回調(diào)
使用 Promise 的時候,我們一般都會調(diào)用 promise 對象的 .then 方法,在 promise 狀態(tài)轉(zhuǎn)為 fulfilled 或 rejected 的時候,拿到內(nèi)部結(jié)果,然后做后續(xù)的處理。所以構(gòu)造函數(shù)中,還需要構(gòu)造兩個數(shù)組,用來存儲 .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)異常直接進行 reject
reject(error)
}
}
}
resolve 與 reject
修改狀態(tài)
接下來,我們需要實現(xiàn) resolve 和 reject 兩個方法,這兩個方法在被調(diào)用的時候,會改變 promise 對象的狀態(tài)。而且任意一個方法在被調(diào)用之后,另外的方法是無法被調(diào)用的。
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('🙆♂️')
}, 500)
setTimeout(() => {
reject('🙅♂️')
}, 800)
}).then(
() => {
console.log('fulfilled')
},
() => {
console.log('rejected')
}
)

此時,控制臺只會打印出 fulfilled ,并不會出現(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)異常直接進行 reject
reject(error)
}
}
}
調(diào)用回調(diào)
修改完狀態(tài)后,拿到結(jié)果的 promise 一般會調(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)異常直接進行 reject
reject(error)
}
}
}
熟悉 JavaScript 事件系統(tǒng)的同學應(yīng)該知道, promise.then 方法中的回調(diào)會被放置到微任務(wù)隊列中,然后異步調(diào)用。

所以,我們需要將回調(diào)的調(diào)用放入異步隊列,這里我們可以放到 setTimeout 中進行延遲調(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)異常直接進行 reject
reject(error)
}
}
}
then 方法
接下來我們需要實現(xiàn) then 方法,用過 Promise 的同學肯定知道,then 方法是能夠繼續(xù)進行鏈式調(diào)用的,所以 then 必須要返回一個 promise 對象。但是在 Promise/A+ 規(guī)范中,有明確的規(guī)定,then 方法返回的是一個新的 promise 對象,而不是直接返回 this,這一點我們可以通過下面代碼驗證一下。

可以看到 p1 對象和 p2 是兩個不同的對象,并且 then 方法返回的 p2 對象也是 Promise 的實例。
除此之外,then 方法還需要判斷當前狀態(tài),如果當前狀態(tài)不是 pending 狀態(tài),則可以直接調(diào)用傳入的回調(diào),而不用再放入隊列進行等待。
class Deferred {
then(onResolve, onReject) {
if (this.status === STATUS.PENDING) {
// 將回調(diào)放入隊列中
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)
// 改變當前 promise 的狀態(tài)
resolve(value)
} catch (error) {
reject(error)
}
})
// 暫存到失敗回調(diào)等待調(diào)用
rejectQueue.push(function (innerValue) {
try {
const value = onReject(innerValue)
// 改變當前 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)可以基本跑通,我們先試運行一段代碼:
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 秒后,控制臺出現(xiàn)如下結(jié)果:

可以看到,這基本符合我們的預期。
值穿透
如果我們在調(diào)用 then 的時候,如果沒有傳入任何的參數(shù),按照規(guī)范,當前 promise 的值是可以透傳到下一個 then 方法的。例如,如下代碼:
new Deferred(resolve => {
resolve(1)
})
.then()
.then()
.then(val => {
console.log(val)
})

在控制臺并沒有看到任何輸出,而切換到 Promise 是可以看到正確結(jié)果的。

要解決這個方法很簡單,只需要在 then 調(diào)用的時候判斷參數(shù)是否為一個函數(shù),如果不是則需要給一個默認值。
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)在我們距離完美實現(xiàn) then 方法只差一步之遙,那就是我們在調(diào)用 then 方法傳入的 onResolve/onReject 回調(diào)時,還需要判斷他們的返回值。如果回調(diào)的內(nèi)部返回的就是一個 promise 對象,我們應(yīng)該如何處理?或者出現(xiàn)了循環(huán)引用,我們又該怎么處理?
前面我們在拿到 onResolve/onReject 的返回值后,直接就調(diào)用了 resolve 或者 resolve ,現(xiàn)在我們需要把他們的返回值進行一些處理。
then(onResolve, onReject) {
// 解決值穿透代碼已經(jīng)省略
if (this.status === STATUS.PENDING) {
// 將回調(diào)放入隊列中
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 的時候,經(jīng)常會在 then 方法中返回一個新的 Promise,然后把新的 Promise 完成后的內(nèi)部結(jié)果再傳遞給后面的 then 方法。
fetch('server/login')
.then(user => {
// 返回新的 promise 對象
return fetch(`server/order/${user.id}`)
})
.then(order => {
console.log(order)
})
function doThenFunc(promise, value, resolve, reject) {
// 如果 value 是 promise 對象
if (value instanceof Deferred) {
// 調(diào)用 then 方法,等待結(jié)果
value.then(
function (val) {
doThenFunc(promise, value, resolve, reject)
},
function (reason) {
reject(reason)
}
)
return
}
// 如果非 promise 對象,則直接返回
resolve(value)
}
判斷循環(huán)引用
如果當前 then 方法回調(diào)函數(shù)返回值是當前 then 方法產(chǎn)生的新的 promise 對象,則被認為是循環(huán)引用,具體案例如下:

then 方法返回的新的 promise 對象 p1 ,在回調(diào)中被當做返回值,此時會拋出一個異常。因為按照之前的邏輯,代碼將會一直困在這一段邏輯里。

所以,我們需要提前預防,及時拋出錯誤。
function doThenFunc(promise, value, resolve, reject) {
// 循環(huán)引用
if (promise === value) {
reject(
new TypeError('Chaining cycle detected for promise')
)
return
}
// 如果 value 是 promise 對象
if (value instanceof Deferred) {
// 調(diào)用 then 方法,等待結(jié)果
value.then(
function (val) {
doThenFunc(promise, value, resolve, reject)
},
function (reason) {
reject(reason)
}
)
return
}
// 如果非 promise 對象,則直接返回
resolve(value)
}
現(xiàn)在我們再試試在 then 中返回一個新的 promise 對象。
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é)果也是完美符合我們的預期。
catch 方法
catch 方法其實很簡單,相當于 then 方法的一個簡寫。
class Deferred {
constructor(callback) {}
then(onResolve, onReject) {}
catch(onReject) {
return this.then(null, onReject)
}
}
靜態(tài)方法
resolve/reject
Promise 類還提供了兩個靜態(tài)方法,直接返回狀態(tài)已經(jīng)固定的 promise 對象。
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 方法接受一個 promise 對象的數(shù)組,等數(shù)組中所有的 promise 對象的狀態(tài)變?yōu)?fulfilled ,然后返回結(jié)果,其結(jié)果也是一個數(shù)組,數(shù)組的每個值對應(yīng)的是 promise 對象的內(nèi)部結(jié)果。
首先,我們需要先判斷傳入的參數(shù)是否為數(shù)組,然后構(gòu)造一個結(jié)果數(shù)組以及一個新的 promise 對象。
class Deferred {
static all(promises) {
// 非數(shù)組參數(shù),拋出異常
if (!Array.isArray(promises)) {
return Deferred.reject(new TypeError('args must be an array'))
}
// 用于存儲每個 promise 對象的結(jié)果
const result = []
const length = promises.length
// 如果 remaining 歸零,表示所有 promise 對象已經(jīng) fulfilled
let remaining = length
const promise = new Deferred(function (resolve, reject) {
// TODO
})
return promise
}
}
接下來,我們需要進行一下判斷,對每個 promise 對象的 resolve 進行攔截,每次 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 = [] // 用于存儲每個 promise 對象的結(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é)果
// 然后運行后面的邏輯
resolve(result)
}
},
reject
)
}
// 放入異步隊列
setTimeout(() => {
for (let i = 0; i < length; i++) {
done(i, promises[i])
}
})
})
return promise
}
}
下面我們通過如下代碼,判斷邏輯是否正確。按照預期,代碼運行后,在 3 秒之后,控制臺會打印一個數(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)
})

上面的運行結(jié)果,基本符合我們的預期。
race
race 方法同樣接受一個 promise 對象的數(shù)組,但是它只需要有一個 promise 變?yōu)?fulfilled 狀態(tài)就會返回結(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)
}
// 放入異步隊列
setTimeout(() => {
for (let i = 0; i < length; i++) {
done(promises[i])
}
})
})
return promise
}
}
下面我們將前面驗證 all 方法的案例改成 race。按照預期,代碼運行后,在 1 秒之后,控制臺會打印一個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)
})

上面的運行結(jié)果,基本符合我們的預期。
總結(jié)
一個簡易版的 Promise 類就已經(jīng)實現(xiàn)了,這里還是省略了部分細節(jié),完整代碼可以訪問 github 。Promise 的出現(xiàn)為后期的 async 語法打下了堅實基礎(chǔ),下一篇博客可以好好聊一聊 JavaScript 的異步編程史,不小心又給自己挖坑了。。。
到此這篇關(guān)于手把手教你實現(xiàn) Promise的方法的文章就介紹到這了,更多相關(guān)Promise語法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何用Node.js編寫內(nèi)存效率高的應(yīng)用程序
這篇文章主要介紹了如何用Node.js編寫內(nèi)存效率高的應(yīng)用程序,對Node.js感興趣的同學,可以參考下2021-04-04
Node.js使用bcrypt-pbkdf實現(xiàn)密碼加密
在這個數(shù)字時代,保護用戶密碼的重要性不言而喻,作為一名資深的前端開發(fā)工程師和技術(shù)博客作者,今天我將帶你詳細了解如何在 Node.js 環(huán)境中利用 bcrypt-pbkdf 模塊進行密碼的哈希處理,確保你的應(yīng)用安全性得到有效提升,需要的朋友可以參考下2024-05-05
nodejs npm install全局安裝和本地安裝的區(qū)別
這篇文章主要介紹了nodejs npm install 全局安裝和非全局安裝的區(qū)別,即帶參數(shù)-g和不帶參數(shù)-g安裝的區(qū)別,需要的朋友可以參考下2014-06-06
Node.js實戰(zhàn)之Buffer和Stream模塊系統(tǒng)深入剖析詳解
這篇文章主要介紹了Node.js實戰(zhàn)之Buffer和Stream模塊系統(tǒng)深入剖析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
手把手教你把nodejs部署到linux上跑出hello world
本篇文章主要介紹了手把手教你把nodejs部署到linux上跑出hello world,非常具有實用價值,需要的朋友可以參考下2017-06-06
nodejs獲取本機內(nèi)網(wǎng)和外網(wǎng)ip地址的實現(xiàn)代碼
這篇文章主要介紹了nodejs獲取本機內(nèi)網(wǎng)和外網(wǎng)ip地址的實現(xiàn)代碼,需要的朋友可以參考下2014-06-06

