深入理解 Promise 之手把手教你寫一版
什麼是 Promise?
-
語法上:
Promise
是一個構造函數,返回一個帶有狀態的對象 -
功能上:
Promise
用於解決異步函數並根據結果做出不同的應對 -
規範上:
Promise
是一個擁有then
方法的對象(在 JS 裏函數也是對象)
爲什麼要用 Promise?
前端最令人頭疼的事情之一就是處理異步請求:
function load() {
$.ajax({
url: 'xxx.com',
data: 'jsonp',
success: function(res) {
init(res, function(res) {
render(res, function(res) {
// 千層餅
});
});
}
}
}
代碼層級多,可讀性差且難以維護,形成回調地獄。
有了 Promise,我們可以用同步操作的流程寫異步操作,解決了層層嵌套的回調函數的困擾:
new Promise(
function (resolve, reject) {
// 一段耗時的異步操作
resolve('成功')
or
reject('失敗')
}
).then(
res => {console.log(res)}, // 成功
).catch(
err => {console.log(err)} // 失敗
)
當然,Promise
也有缺點
-
無法取消
Promise
,一旦新建就會立即執行,無法中途取消 -
如果不設置回調函數,無法拋出
Promise
內部錯誤到外部 -
當處於
Pending
狀態時,無法得知目前運行的情況,是剛開始還是快結束
事不宜遲,我們馬上開始!
Promise 的狀態
Promise
有以下 3 種狀態:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
狀態只能由 pending
向 fulfilled
或 rejected
轉變,且只有在執行環境堆棧僅包含平臺代碼時轉變一次,稱爲狀態凝固,並保存一個參數表明結果。
this.value = value // fulfilled狀態,保存終值
this.reason = reason // rejected狀態,保存據因
Promise 構造函數
promise
構造函數接受一個函數作爲參數,我們稱該函數參數爲 executor
,待 promise
執行時,會向 executor
傳入兩個函數參數,分別爲 resolve
和 reject
,它們只做 3 件事:
-
改變
promise
狀態 -
保存
value/reason
結果 -
執行
onFulfilled/onRejected
回調函數
其中第三條即爲 then
方法中配置的回調函數,這裏先不做多討論,先看前兩條,只需要兩行代碼即可:
this.state = state
this.value = value / this.reason = reason
我們先手擼一個簡單的構造函數:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
constructor(executor) {
this.state = PENDING // prpmise具有初始狀態
this.value = null// 用於保存終值
this.reason = null// 用於保存拒因
this.onFulfilledCallbacks = [] // 成功回調隊列
this.onRejectedCallbacks = [] // 失敗回調隊列
// 定義 resolve 函數
// 這裏使用箭頭函數以解決 this 的指向,不瞭解的朋友可以先看阮大大的ES6文章
const resolve = value => {
// 保證狀態只能改變一次
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
}
}
// 同上
const reject = reason => {
if (this.state === PENDING) {
this.state = REJECTED
this.reason = reason
}
}
// executor 可能會出現異常,需要捕獲並調用reject函數表示執行失敗
try {
// 傳入兩個函數參數
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
}
看上去還不錯,大概的流程已經完成了。還記得之前說過,狀態的改變是處於主線程空閒時,這裏使用 setTimeout
來模擬,以及 resolve/reject
還剩下第 3 件事,現在就讓我們一起完善它吧:
const resolve = value => {
// setTimeout 模擬
// 注意 即便是判斷狀態是否爲 pending 也應該要在主線程空閒時執行
setTimeout(() => {
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
// 若是使用 forEach 回調函數有可能不按順序執行
this.onFulfilledCallbacks.map(cb => cb(this.value))
}
})
}
// reject同上
好啦,一個完整的構造函數就寫完了,是不是覺得很輕鬆,Promise
不過如此。
接下來是重頭戲 then
方法,then
接受兩個函數參數,分別爲 onFulfilled/onRejected
,用來配置 promise
狀態改變後的回調函數。
其有兩個重點:
- 返回一個
promise2
,以實現鏈式調用
-
其中
promise2
的狀態必須要凝固 -
通過
resolvePromise
函數以及onFulfilled/onRejected
的返回值來實現promise2
的狀態凝固
- 監聽或執行對應的
onFulfilled/onRejected
回調函數
-
若是執行則需放入
event-loop
-
監聽只需推入回調函數數組中
上述的 resolvePromise
我們先不理會,只要知道它是用來決定 promise2
的狀態即可。
首先,then
需要返回一個 promise2
:
then(onFulfilled, onRejected) {
let promise2
return (promise2 = new MyPromise((resolve, reject) => {
})
}
其次,then
方法的目的是配置或執行對應的 onFulfilled/onRejected
回調函數:
then(onFulfilled, onRejected) {
let promise2
return (promise2 = new MyPromise((resolve, reject) => {
// 將回調函數配置好並推入對應的 callbacks 數組中
this.onFulfilledCallbacks.push(value => {
// 配置第一步:執行 callback 並保存返回值 x
let x = onFulfilled(value);
// 配置第二步:通過 resolvePromise 決定 promise2 狀態
resolvePromise(promise2, x, resolve, reject)
})
// onRejected 同上
this.onRejectedCallbacks.push(reason => {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
})
})
}
在這裏可以大概瞭解 resolvePromise
是如何改變 promise2
狀態的,它接受 promise2
的 resolve/reject
,由於箭頭函數的原因,resolve/reject
的 this
指向依舊指向 promise2
,從而可以通過 resolvePromise
來改變狀態。
萬一 onFulfilled/onRejected
出錯怎麼辦?我們需要將它捕獲並將 promise2
的狀態改爲 rejected
,我們將代碼再做修改:
then(onFulfilled, onRejected) {
let promise2
return (promise2 = new MyPromise((resolve, reject) => {
// 將回調函數配置好並推入對應的 callbacks 數組中
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
// onRejected 同上
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
}
如果調用 then
方法的是已經狀態凝固的 promise
呢,也要推入 callbacks
數組嗎?答案當然不是,而是直接將配置好的 onFulfilled/onRejected
扔入 event-loop
中,就不勞煩 resolve/reject
了:
then(onFulfilled, onRejected){
// fulfilled 狀態,將配置好的回調函數扔入 event-loop
if (this.state === FULFILLED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
// rejected 狀態同上
if (this.state === REJECTED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
// pending 狀態則交由 resolve/reject 來決定
if (this.state === PENDING) {
return (promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
}
}
看上去完美了,不過還差一件小事,假如 promise
使用者不按套路出牌,傳入的 onFulfilled/onRejected
不是一個函數怎麼辦?這裏我們就直接將之作爲返回值直接返回:
then(onFulfilled, onRejected){
let promise2
// 確保 onFulfilled/onRejected 爲函數
// 若非函數,則轉換爲函數並且返回值爲自身
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => {
throw reason
}
if (this.state === FULFILLED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
if (this.state === REJECTED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
if (this.state === PENDING) {
return (promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
}
}
大功告成!
最後只剩下一個 resolvePromise
方法,先介紹一下它的功能:根據回調函數的返回值 x
決定 promise2
的最終狀態:
-
如果
x
爲thenable
對象,即帶then
方法的對象 -
有,因其不一定符合
promise
的標準,我們做多一些準備 -
無,當作普通值執行
-
使用
called
變量使得其狀態改變只能發生一次 -
監聽異常
-
遞歸調用 resolvePromise 以防止出現套娃
-
如果
x
爲promise
,則遞歸調用,直到返回值爲普通值爲止 -
如果
x
爲函數或對象,判斷其有無then
方法 -
x
爲普通值 -
直接返回
讓我們來一步一步刨析它吧:
function resolvePromise(promise2, x, resolve, reject){
// 先從 x 爲 thenable 對象開始
// 如果 x === promise2 需要拋出循環引用錯誤,否則會死循環
if (x === promise2) {
reject(newTypeError('循環引用'))
}
// 如果 x 就是 promise
// 根據 x 的狀態來決定 promise2 的狀態
if (x instanceof MyPromise) {
// x 狀態爲 PENDING 時
// 當 x 被 resolve 會調用新的 resolvePromise
// 因爲怕 resolve 保存的終值還是 promise 繼續套娃
// 所以一定要遞歸調用 resolvePromise 保證最終返回的一定是普通值
// 失敗直接調用 reject 即可
if (x.state === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject)
},
r => {
reject(r)
}
)
} else {
// x 狀態凝固,直接配置即可
// 不過這裏有個疑問
// 如果之前 resolve 保存的終值還是 promise 呢
// 該怎樣預防這一問題,後續將會講到
x.then(resolve, reject)
}
}
}
現在把應對 x
的值爲 promise
的代碼書寫完畢,但這還不夠,我們要面對的不只是 promise
,而是一個 thenable
對象,所以還要繼續判斷:
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(newTypeError('循環引用'))
}
if (x instanceof MyPromise) {
// 前面的代碼不再贅述
} elseif (x && (typeof x === 'function' || typeof x === 'object')) {
// 因爲不一定是規範的 promise 對象
// 我們需要保證狀態的改變只發生一次
// 加入一個 called 變量來加鎖
let called = false
// 還是因爲不一定是規範的 promise 對象
// 需要保證運行時異常能夠被捕獲
try {
// 注意,前面不加 try/catch
// 僅僅下面這一行代碼也有可能會報錯而無法被捕獲
let then = x.then
// 假如 x.then 存在併爲函數
if (typeof then === 'function') {
// 使用 call 方法保證 then 的調用對象爲 x
then.call{
x,
y => {
// 假如狀態凝固便不再執行
if (called) return
called = true
// 防止出現 resolve 保存 promise 的情況
resolvePromise(promise2, y, resolve, reject)
},
r => {
// 同上
if (called) return
called = true
reject(r)
}
}
} else {
// 如果 x.then 不是函數
// 即爲普通值,直接 resolve 就好
resolve(x)
}
} catch (e) {
// 若調用一個不正規的 thenalbe 對象出錯
// 拋出異常
// 這裏要注意,這裏出現錯誤很有可能是執行了 x.then 方法,而之前也說過,其不一定正規,可能狀態已經凝固,需要多加一重保險
if (called) return
called = true
reject(e)
}
} else {
// 不是 thenable 對象,那就是普通值
// 直接 resolve
resolve(x)
}
}
一套行雲流水的代碼寫下來,我們的 promise
就完成了,不過還記得之前代碼裏留了個疑問嗎?當 x
爲 promise
且狀態凝固時,如果確定它保存的終值的不是 promise
呢?其實只要最開始的 resolve
函數多加一重判斷即可:
const resolve = value => {
if (value instanceof MyPromise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
this.onFulfilledCallbacks.map(cb => cb(this.value))
}
})
}
再次防止套娃!
好啦,也許你會問,我怎麼知道這個手寫的 promise
就一定是正確的呢?接下來將一步步帶你驗證!
首先找到一個空文件夾,在命令行輸入:
npm init -y
// 下載 promise 測試工具
npm install promises-aplus-tests -D
新建 promise.js
文件,並將你實現的 promise
複製於此,並在下方加入一下代碼:
MyPromise.deferred = function() {
let defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
});
return defer
}
module.exports = MyPromise
再修改 package.json
文件如下:
{
"name": "promise",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "promises-aplus-tests ./promise.js"
},
"devDependencies": {
"promises-aplus-tests": "^2.1.2"
}
}
最後一步:
npm run test
完成!
手寫 Promise
,你也可以!
最後附上完整實現代碼
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.state = PENDING
this.value = null
this.reason = null
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = value => {
if (value instanceof MyPromise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
this.onFulfilledCallbacks.map(cb => cb(this.value))
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.state === PENDING) {
this.state = REJECTED
this.reason = reason
this.onRejectedCallbacks.map(cb => cb(this.reason))
}
})
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
let promise2
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => {
throw reason
}
if (this.state === FULFILLED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
if (this.state === REJECTED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
if (this.state === PENDING) {
return (promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}))
}
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(newTypeError('循環引用'))
}
if (x instanceof MyPromise) {
if (x.state === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject)
},
r => {
reject(r)
}
)
} else {
x.then(resolve, reject)
}
} elseif (x && (typeof x === 'function' || typeof x === 'object')) {
let called = false
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
},
r => {
if (called) return
called = true
reject(r)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
MyPromise.deferred = function() {
let defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
});
return defer
};
module.exports = MyPromise;
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_gDIO6YCswAS2dICllMG0A