深入理解 Promise 之手把手教你寫一版

語言: CN / TW / HK

什麼是 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;

緊追技術前沿,深挖專業領域

掃碼關注我們吧!