深入理解 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;
緊追技術前沿,深挖專業領域
掃碼關注我們吧!
- 基於 qiankun 的微前端實踐
- 基於 qiankun 的微前端實踐
- 動態監聽DOM元素高度變化
- 解決前端常見問題:競態條件
- Scroll,你玩明白了嘛?
- 深入理解 Promise 之手把手教你寫一版
- 文件解析和DOMContentLoaded觸發時機
- 【TypeScript】never 和 unknown 的優雅之道
- 【音影片掃盲】可分級影片編碼
- 基於 Yarn 的 Monorepo 實踐
- WebSocket 基礎與應用系列(二)—— Engine.IO 原理了解
- Node.js 流原始碼解讀之可讀流
- Dark Mode 實踐踩坑記錄
- 帶你深入瞭解 useState
- Nohost 實現原理 —— 遠端辦公下的開發測試協同提效方案
- Jest React Testing Library 單測總結
- OpenGL 圖形渲染流程入門
- 又過年了,盤一盤近一年閱讀量最高的十篇文章
- 影片中的 I 幀,P 幀,B 幀
- “和 loading 介面說 ByeBye”—— Remix 顛覆式預載入解析