前端雜談: 如何實現一個 Promise?
前端雜談: 如何實現一個 Promise?
首先, 什麼是 Promise?
A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). A promise may be in one of 3 possible states: fulfilled, rejected, or pending. Promise users can attach callbacks to handle the fulfilled value or the reason for rejection.
關鍵語句: Promise 是一個在將來某個時刻產生一個單一結果的物件. 通俗一點來說, Promise 代表了一個值, 但是這個值我們並不確定什麼時候會被返回.
A promise is an object that may produce a single value some time in the future.
簡單看看 Promise 的歷史
-
Promise 在 1980 年代被創建出來
-
在 1988 年正式得名: Promise
-
已經有很多人瞭解到了 Promise, 但是人們還是堅持使用 node.js 中提倡的以回撥函式首個引數傳 error 物件的方式處理非同步程式碼.
-
Dojo 首次大規模的使用了 Promise , 相應的 Promise/A 被提出用以規範 Promise 的實現
-
JQuery 開始使用 Promise 並真正使 Promise 廣為人知
-
JQuery 沒有實現部分 Promise 的功能, 這也導致了 Promie/A+ 標準的產生
-
ES6 正式引入了 Promise,並且和已有的實現了 Promise/A 規範的 library 相相容
實現 Promise 之前, 讓我們看看 Promise 有哪些規範
- Promise 是一個
thenable
物件, 也就是說 Promise 有一個.then()
方法 - 一個 pending 狀態的 Promise 可以進入 fulfilled 和 rejected 狀態
- promise 一旦進入 fulfilled 或 rejected 狀態, 不可再改變其狀態
- 一旦 promise 改變了其狀態, 它筆芯有一個值(這個值也可能是 undefined)
開始實現一個 Promise
首先, 讓我們看看一段最普通的非同步程式碼:
// 非同步方法定義
var basicAsyncFunc = function(callback) {
setTimeout(function() {
var randomNumber = Math.random()
if (randomNumber > 0.5) {
callback(null, randomNumber)
} else {
callback(new Error('bad luck...'))
}
}, 1000)
}
// 非同步方法呼叫
basicAsyncFunc((err, result) => {
if (err) {
console.log(`the reason fail is: ${err}`)
return
}
console.log(`success get result: ${result}`)
})
按照 Promise 的規範定義, 理想中 Promise 的呼叫方式為:
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {}
// Promise 形式的非同步方法呼叫
promiseAsyncFunc.then(
data => {
console.log(`success get result: ${data}`)
},
err => {
console.log(`the reason fail is: ${err}`)
}
)
按照這個理想當中的呼叫方式, 讓我們寫出第一版程式碼.
第一版 Promise:能儲存回撥方法
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var fulfillCallback
var rejectCallback
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfillCallback(randomNumber)
else rejectCallback(randomNumber)
}, 1000)
return {
then: function(_fulfillCallback, _rejectCallback) {
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
// Promise 形式的非同步方法呼叫
promiseAsyncFunc().then(fulfillCallback, rejectCallback)
我們的思路是在 .then()
方法中, 將 fullfill 和 reject 結果的回撥函式儲存下來, 然後在非同步方法中呼叫. 因為是非同步呼叫, 根據 event-loop 的原理, promiseAsyncFunc().then(fulfillCallback, rejectCallback)
傳入的 callback 在非同步呼叫結束時一定是已經賦值過了.
第二版 Promise:實建構函式
當前我們的實現 Promise 中,非同步邏輯程式碼和 Promise 的程式碼是雜糅在一起的,讓我們將其區分開:
var promiseAsyncFunc = function() {
var fulfillCallback
var rejectCallback
return {
fulfill: function(value) {
if (fulfillCallback && typeof fulfillCallback === 'function') {
fulfillCallback(value)
}
},
reject: function(err) {
if (rejectCallback && typeof rejectCallback === 'function') {
rejectCallback(err)
}
},
then: function(_fulfillCallback, _rejectCallback) {
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
let ownPromise = function(asyncCall) {
let promise = promiseAsyncFunc()
asyncCall(promise.fulfill, promise.reject)
return promise
}
// Promise 形式的非同步方法呼叫
ownPromise(function(fulfill, reject) {
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfill(randomNumber)
else reject(randomNumber)
}, 1000)
})
我們新定義了一個方法 ownPromise()
用於建立 Promise,並在promiseAsyncFunc()
中暴露出 fulfill
和 reject
介面方便非同步程式碼去呼叫。
這裡有一個問題,我們在呼叫 ownPromise()
後得到了 promise 例項,此時我們可以直接呼叫 fulfill()
,reject()
這兩個方法,而理論上我們應該只應暴露 promise 的then()
方法。所以我們利用閉包將這兩個方法隱藏:
var promiseAsyncFunc = function() {
var fulfillCallback
var rejectCallback
return {
fulfill: function(value) {
if (fulfillCallback && typeof fulfillCallback === 'function') {
fulfillCallback(value)
}
},
reject: function(err) {
if (rejectCallback && typeof rejectCallback === 'function') {
rejectCallback(err)
}
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
}
let ownPromise = function(asyncCall) {
let defer = promiseAsyncFunc()
asyncCall(defer.fulfill, defer.reject)
return defer.promise
}
// Promise 形式的非同步方法呼叫
ownPromise(function(fulfill, reject) {
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfill(randomNumber)
else reject(randomNumber)
}, 1000)
})
第三版 Promise: 支援狀態管理
為了實現規範中對於 Promise 狀態變化的要求, 我們需要為 Promise 加入狀態管理, 這一步較為簡單, 讓我們看程式碼:
const PENDING = Symbol('pending')
const FULFILLED = Symbol('fulfilled')
const REJECTED = Symbol('rejected')
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
return {
fulfill: function(value) {
if (status !== PENDING) return
if (typeof fulfillCallback === 'function') {
fulfillCallback(value)
status = FULFILLED
}
},
reject(error) {
if (status !== PENDING) return
if (typeof rejectCallback === 'function') {
rejectCallback(error)
status = REJECTED
}
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
}
let ownPromise = function(asyncCall) {
let defer = promiseAsyncFunc()
asyncCall(defer.fulfill, defer.reject)
return defer.promise
}
// Promise 形式的非同步方法呼叫
ownPromise(function(fulfill, reject) {
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfill(randomNumber)
else reject(randomNumber)
}, 1000)
}).then(data => console.log(data), err => console.log(err))
這段程式碼中我們用到了 Symbol 來表示狀態常量, 對 Symbol 不瞭解的同學可以看這裡
為了判斷 Promise 的狀態, 我們加入了 fulfill
和 reject
兩個方法。並在其中判斷 promise 當前狀態。如果不是 pending 狀態則直接 return(因為 Promise 狀態只可能改變一次)。
現在我們的 promise 實現了對狀態控制的規範:
- 只允許改變一次狀態
- 只能從 pending => fulfilled 或 pending => rejected
但是我們的 Promise 有一個問題: promise 的值沒有被儲存下來。如果 promise 在非同步呼叫完成之後才被呼叫 .then()
方法,則我們無法把非同步呼叫的結果傳遞給回撥函式。為此我們需要為 Promise 加一個 value 欄位:
第四版 Promise: 儲存非同步呼叫的結果
我們為 promise 加入 value 欄位,用於儲存 Promise 的執行結果。
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
var value
return {
fulfill: function(_value) {
if (status !== PENDING) return
value = _value
status = FULFILLED
if (typeof fulfillCallback === 'function') {
fulfillCallback(value)
}
},
reject(error) {
if (status !== PENDING) return
value = error
status = REJECTED
if (typeof rejectCallback === 'function') {
rejectCallback(error)
}
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
}
這裡我們又發現一個問題,如果一個 Promise 已經是fulfill
或reject
狀態。我們再呼叫 then()
方法時,傳入的回撥方法永遠不會被呼叫(因為 status 已經不是 pending)。
所以我們需要在 then()
方法中對其狀態進行判斷:
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
var value
return {
fulfill: function(_value) {
if (status !== PENDING) return
value = _value
status = FULFILLED
if (typeof fulfillCallback === 'function') {
fulfillCallback(value)
}
},
reject(error) {
if (status !== PENDING) return
value = error
status = REJECTED
if (typeof rejectCallback === 'function') {
rejectCallback(error)
}
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
if (status === REJECTED) {
_rejectCallback(value)
return
}
if (status === FULFILLED) {
_fulfillCallback(value)
return
}
fulfillCallback = _fulfillCallback
rejectCallback = _rejectCallback
}
}
}
}
第五版 Promise: 支援鏈式呼叫
為了支援鏈式呼叫,.then()
方法的返回值必須是用 thenable
(根據 Promise/A+ 規範, .then()
方法的返回值需要是一個新的 Promise)
為此我們加入一個工具方法 makeThenable()
。如果傳入的 value 本身就有 then()
方法,則直接返回 value。否則返回一個有 then()
方法的物件。
在該物件的 then()
方法中,我們根據 promise 的狀態,呼叫不同的回撥方法生成新的 value。
function makeThenable(value, status){
if(value && typeof value.then === 'function'){
return value
}
if(status === FULFILLED){
return {
then: function(fulfillCallback, rejectCallback){
return makeThenable(fulfillCallback(value), FULFILLED)
}
}
}
if(status === REJECTED) {
return {
then: function(fulfillCallback, rejectCallback){
return makeThenable(rejectCallback(value), FULFILLED)
}
}
}
}
有了以上的 makeThenable()
方法,我們可以在 promise 的fulfill()
,reject()
回將 value 設定為 thenable
:
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
var value
return {
fulfill: function(_value) {
if (status !== PENDING) return
value = makeThenable(_value, FULFILLED) // 保證當前promise的value為 thenable
status = FULFILLED
if (typeof fulfillCallback === 'function') {
value.then(fulfillCallback)
}
},
reject(error) {
if (status !== PENDING) return
value = makeThenable(error, REJECTED) 、、 // 保證當前value為 thenable
status = REJECTED
if (typeof rejectCallback === 'function') {
value.then(null, rejectCallback)
}
},
promise: {
then: function(){}
}
}
}
接下來讓我們看 then()
方法。為了返回一個新的 promise,我們首先得建立一個新的 promise。其次當前 promise 在fulfill()
或 reject()
時,應該呼叫新的 promise 的fullfill()
或 reject()
方法。所以我們在將 fulfullCallback
和rejectCallback
賦值給當前 promise 時,將其包裝一下。程式碼如下:
promise: {
then: function(_fulfillCallback, _rejectCallback) {
let newPromiseAsyncFunc = promiseAsyncFunc()
let fulfillFunc = function(value) {
newPromiseAsyncFunc.fulfill(_fulfillCallback(value))
}
let rejectFunc = function(err) {
newPromiseAsyncFunc.fulfill(_rejectCallback(err))
}
if (status === PENDING) {
fulfillCallback = fulfillFunc
rejectCallback = rejectFunc
} else {
value.then(fulfillFunc, rejectFunc)
}
return newPromiseAsyncFunc.promise
}
}
如此,我們變得到了一個可以鏈式呼叫的 promise。讓我們來測試一下:
const PENDING = Symbol('pending')
const FULFILLED = Symbol('fulfilled')
const REJECTED = Symbol('rejected')
function makeThenable(value, status) {
if (value && typeof value.then === 'function') {
return value
}
if (status === FULFILLED) {
return {
then: function(fulfillCallback, rejectCallback) {
return makeThenable(fulfillCallback(value), FULFILLED)
}
}
}
if (status === REJECTED) {
return {
then: function(fulfillCallback, rejectCallback) {
return makeThenable(rejectCallback(value), FULFILLED)
}
}
}
}
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
var value
return {
fulfill: function(_value) {
if (status !== PENDING) return
value = makeThenable(_value, FULFILLED)
status = FULFILLED
if (typeof fulfillCallback === 'function') {
value.then(fulfillCallback)
}
},
reject(error) {
if (status !== PENDING) return
value = makeThenable(error, REJECTED)
status = REJECTED
if (typeof rejectCallback === 'function') {
value.then(null, rejectCallback)
}
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
let newPromiseAsyncFunc = promiseAsyncFunc()
let fulfillFunc = function(value) {
newPromiseAsyncFunc.fulfill(_fulfillCallback(value))
}
let rejectFunc = function(err) {
newPromiseAsyncFunc.fulfill(_rejectCallback(err))
}
if (status === PENDING) {
fulfillCallback = fulfillFunc
rejectCallback = rejectFunc
} else {
value.then(fulfillFunc, rejectFunc)
}
return newPromiseAsyncFunc.promise
}
}
}
}
let ownPromise = function(asyncCall) {
let defer = promiseAsyncFunc()
asyncCall(defer.fulfill, defer.reject)
return defer.promise
}
let testChainedPromise = ownPromise(function(fulfill, reject) {
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfill(randomNumber)
else reject(randomNumber)
}, 1000)
})
.then(
data => {
console.log(data)
return 'return value in then1 fulfill'
},
err => {
console.log(err)
return 'return value in then1 reject'
}
)
.then(
data => {
console.log(data)
return 'return value in then2 fulfill'
},
err => {
console.log(err)
return 'return value in then2 reject'
}
)
.then(
data => {
console.log(data)
},
err => {
console.log(err)
}
)
/**
console output:
0.9931984611850693
return value in then1 fulfill
return value in then2 fulfill
*/
第六版 Promise: Error handling
這裡我們只對非同步呼叫和fulfill 回撥中丟擲的 error 進行處理。
首先是非同步呼叫部分,我們將其 try catch
起來,在發生異常時呼叫 reject 方法,並將異常作為引數傳入。
let ownPromise = function(asyncCall) {
let defer = promiseAsyncFunc()
try {
asyncCall(defer.fulfill, defer.reject)
} catch (e) {
defer.reject(e)
}
return defer.promise
}
然後是 fulfill
中可能出現的異常。我們對fulfillCallback(value)
可能出現的異常進行捕獲,並將異常傳遞給rejectCallback
。
function makeThenable(value, status) {
if (value && typeof value.then === 'function') {
return value
}
if (status === FULFILLED) {
return {
then: function(fulfillCallback, rejectCallback) {
try {
let newValue = fulfillCallback(value)
return makeThenable(newValue, FULFILLED)
} catch (e) {
return makeThenable(rejectCallback(e), FULFILLED)
}
}
}
}
if (status === REJECTED) {
return {
then: function(fulfillCallback, rejectCallback) {
return makeThenable(rejectCallback(value), FULFILLED)
}
}
}
}
最後讓我們對完整的程式碼進行測試:
const PENDING = Symbol('pending')
const FULFILLED = Symbol('fulfilled')
const REJECTED = Symbol('rejected')
function makeThenable(value, status) {
if (value && typeof value.then === 'function') {
return value
}
if (status === FULFILLED) {
return {
then: function(fulfillCallback, rejectCallback) {
try {
let newValue = fulfillCallback(value)
return makeThenable(newValue, FULFILLED)
} catch (e) {
return makeThenable(rejectCallback(e), FULFILLED)
}
}
}
}
if (status === REJECTED) {
return {
then: function(fulfillCallback, rejectCallback) {
return makeThenable(rejectCallback(value), FULFILLED)
}
}
}
}
// Promise 形式的非同步方法定義
var promiseAsyncFunc = function() {
var status = PENDING
var fulfillCallback
var rejectCallback
var value
return {
fulfill: function(_value) {
if (status !== PENDING) return
value = makeThenable(_value, FULFILLED)
status = FULFILLED
if (typeof fulfillCallback === 'function') {
value.then(fulfillCallback)
}
},
reject(error) {
if (status !== PENDING) return
value = makeThenable(error, REJECTED)
if (typeof rejectCallback === 'function') {
value.then(null, rejectCallback)
}
status = REJECTED
},
promise: {
then: function(_fulfillCallback, _rejectCallback) {
let newPromiseAsyncFunc = promiseAsyncFunc()
let fulfillFunc = function(value) {
newPromiseAsyncFunc.fulfill(_fulfillCallback(value))
}
let rejectFunc = function(err) {
newPromiseAsyncFunc.fulfill(_rejectCallback(err))
}
if (status === PENDING) {
fulfillCallback = fulfillFunc
rejectCallback = rejectFunc
} else {
value.then(fulfillFunc, rejectFunc)
}
return newPromiseAsyncFunc.promise
}
}
}
}
let ownPromise = function(asyncCall) {
let defer = promiseAsyncFunc()
try {
asyncCall(defer.fulfill, defer.reject)
} catch (e) {
defer.reject(e)
}
return defer.promise
}
let testChainedPromise = ownPromise(function(fulfill, reject) {
throw Error('here is an error in asyncCall')
setTimeout(() => {
var randomNumber = Math.random()
if (randomNumber > 0.5) fulfill(randomNumber)
else reject(randomNumber)
}, 1000)
})
.then(
data => {
console.log(data)
return 'return value in then1 fulfill'
},
err => {
console.log(err.message)
return 'return value in then1 reject'
}
)
.then(
data => {
console.log(data)
throw Error('here is an error in fulfill1')
return 'return value in then2 fulfill'
},
err => {
console.log(err.message)
return 'return value in then2 reject'
}
)
.then(
data => {
console.log(data)
},
err => {
console.log(err.message)
}
)
// console out:
Error: here is an error in asyncCall
return value in then1 reject
Error: here is an error in fulfill1
return value in then2 reject
總結
以上就是我們對於 Promise 的一個簡單的實現,實現思路主要參考了 Q-A promise library for javascript。該實現的 Promise 功能較為簡陋,僅實現了部分 api/規範。有任何意見和建議歡迎在評論區交流 ;)
進一步閱讀 && 引用
對於 Promise 使用以及error handle 的講解:
https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261
Promise 實現庫之一: Q 對於 Promise 實現的講解:
想了解更多 前端 和 資料視覺化 ?
這裡是我的 前端、D3.js 、 資料視覺化 的 github 地址, 歡迎 star & fork :tada: