前言
眾所周知,ES6中的Promise對象是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件,更合理和強大,目前也得到了前端領(lǐng)域的廣泛使用。不僅是項目實踐中常用,面試中也經(jīng)常出現(xiàn)。
Promise的使用想必大家都很熟練,可究其內(nèi)部原理,很多人都是一知半解。這導(dǎo)致面試中出現(xiàn)Promise原理甚至要求手動封裝時,很多人都會掛掉。本著知其然,也要知其所以然的目的,開始對Promise進行了探索。
一、為什么要使用Promise
大家都知道JavaScript一大特點就是單線程,為了不阻塞主線程,有些耗時操作(比如ajax)必須放在任務(wù)隊列中異步執(zhí)行。傳統(tǒng)的異步編程解決方案之一回調(diào),很容易產(chǎn)生臭名昭著的回調(diào)地獄問題。
setTimeout(function () {
console.log('延時觸發(fā)');
}, 2000);
fs.readFile('./sample.txt', 'utf-8', function (err, res) {
console.log(res);
});
上面就是典型的回調(diào)函數(shù),不論是在瀏覽器中,還是在node中,JavaScript本身是單線程,因此,為了應(yīng)對一些單線程帶來的問題,異步編程成為了JavaScript中非常重要的一部分。
不論是瀏覽器中最為常見的ajax、事件監(jiān)聽,還是node中文件讀取、網(wǎng)絡(luò)編程、數(shù)據(jù)庫等操作,都離不開異步編程。在異步編程中,許多操作都會放在回調(diào)函數(shù)(callback)中。同步與異步的混雜、過多的回調(diào)嵌套都會使得代碼變得難以理解與維護,這也是常受人詬病的地方。
回調(diào)嵌套過多后,你的代碼會變成這樣:
asyncFunc1(opt, (...args1) => {
asyncFunc2(opt, (...args2) => {
asyncFunc3(opt, (...args3) => {
asyncFunc4(opt, (...args4) => {
// some operation
});
});
});
});
左側(cè)明顯出現(xiàn)了一個三角形的縮進區(qū)域,過多的回調(diào)也就讓我們陷入“回調(diào)地獄”。
雖然回調(diào)地獄可以通過減少嵌套、模塊化等方式來解決,但我們有更好的方案可以采取,那就是Promise。
二、含義與規(guī)范
Promise 是一個對象,保存著異步操作的結(jié)果,在異步操作結(jié)束后,會變更 Promise 的狀態(tài),然后調(diào)用注冊在 then 方法上回調(diào)函數(shù)。
實際上,Promise 是對 Promises/A+ 規(guī)范的一種實現(xiàn)。 ES6 原生提供了 Promise 對象,統(tǒng)一了用法。
三、封裝Promise
下面將依據(jù)規(guī)范和ES6來進行Promise的封裝
1.promise構(gòu)造函數(shù)
規(guī)范沒有指明如何書寫構(gòu)造函數(shù),那就參考ES6的構(gòu)造方式:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是 resolve 和 reject
resolve 函數(shù)的作用是將 Promise 對象的狀態(tài)從 pending 變?yōu)?fulfilled ,在異步操作成功時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞給注冊在 then 方法上的回調(diào)函數(shù)(then方法的第一個參數(shù)); reject 函數(shù)的作用是將 Promise 對象的狀態(tài)從 pending 變?yōu)?rejected ,在異步操作失敗時調(diào)用,并將異步操作報出的錯誤,作為參數(shù)傳遞給注冊在 then 方法上的回調(diào)函數(shù)(then方法的第二個參數(shù))
所以我們要實現(xiàn)的 promise (小寫以便區(qū)分ES6的 Promise )構(gòu)造函數(shù)大體如下:
// promise 構(gòu)造函數(shù)
function promise (fn) {
let that = this
that.status = 'pending' // 存儲promise的state
that.value = '' // 存儲promise的value
that.reason = '' // 存儲promise的reason
that.onFulfilledCb = [] // 存儲then方法中注冊的回調(diào)函數(shù)(第一個參數(shù))
that.onRejectedCb = [] // 存儲then方法中注冊的回調(diào)函數(shù)(第二個參數(shù))
// 2.1
function resolve (value) {
// 將promise的狀態(tài)從pending更改為fulfilled,并且以value為參數(shù)依次調(diào)用then方法中注冊的回調(diào)
setTimeout (() => {
if (that.status === 'pending') {
that.status = 'fulfilled'
that.value = value
// 2.2.2、2.2.6
that.onFulfilledCb.map( item => {
item(that.value)
})
}
}, 0)
}
function reject (reason) {
// 將promise的狀態(tài)從pending更改為rejected,并且以reason為參數(shù)依次調(diào)用then方法中注冊的回調(diào)
setTimeout(() => {
if (that.status === 'pending') {
that.status = 'rejected'
that.reason = reason
// 2.2.3、2.2.6
that.onRejectedCb.map( item => {
item(that.reason)
})
}
}, 0)
}
fn(resolve, reject)
}
規(guī)范2.2.6中明確指明 then 方法可以被同一個 promise 對象調(diào)用,所以這里需要用一個數(shù)組 onFulfilledCb 來存儲then方法中注冊的回調(diào)
這里我們執(zhí)行 resolve reject 內(nèi)部代碼使用setTimeout,是為了確保 then 方法上注冊的回調(diào)能異步執(zhí)行(規(guī)范3.1)
2.then方法
promise 實例具有 then 方法,也就是說,then方法是定義在原型對象 promise.prototype 上的。它的作用是為 promise 實例添加狀態(tài)改變時的回調(diào)函數(shù)。
規(guī)范2.2
promise必須提供一個then方法promise.then(onFulfilled,onRejected)
規(guī)范2.2.7then方法必須返回一個新的promise
閱讀理解規(guī)范2.1和2.2,我們也很容易對then方法進行實現(xiàn):
promise.prototype.then = function (onFulfilled, onRejected) {
let that = this
let promise2
// 2.2.1、2.2.5
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => r
if (that.status === 'pending') {
// 2.2.7
return promise2 = new promise((resolve, reject) => {
that.onFulfilledCb.push(value => {
try {
let x = onFulfilled(value)
} catch(e) {
// 2.2.7.2
reject(e)
}
})
that.onRejectedCb.push(reason => {
try {
let x = onRejected(reason)
} catch(e) {
// 2.2.7.2
reject(e)
}
})
})
}
}
重點在于對 onFulfilled 、 onRejected 函數(shù)的返回值x如何處理,規(guī)范中提到一個概念叫 PromiseResolutionProcedure ,這里我們就叫做Promise解決過程
Promise 解決過程是一個抽象的操作,需要輸入一個 promise 和一個值,我們表示為 [[Resolve]] (promise,x),如果 x 有 then 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態(tài);否則用 x的值來執(zhí)行promise。
3.promise解決過程
對照規(guī)范2.3,我們再來實現(xiàn) promise resolution , promise resolution 針對x的類型做了各種處理:如果 promise 和 x 指向同一對象,以 TypeError 為 reason 拒絕執(zhí)行 promise、如果 x 為 promise ,則使 promise 接受 x 的狀態(tài)、如果 x 為對象或者函數(shù),判斷 x.then 是否是函數(shù)、 如果 x 不為對象或者函數(shù),以 x 為參數(shù)執(zhí)行 promise(resolve和reject參數(shù)攜帶promise2的作用域,方便在x狀態(tài)變更后去更改promise2的狀態(tài))
// promise resolution
function promiseResolution (promise2, x, resolve, reject) {
let then
let thenCalled = false
// 2.3.1
if (promise2 === x) {
return reject(new TypeError('promise2 === x is not allowed'))
}
// 2.3.2
if (x instanceof promise) {
x.then(resolve, reject)
}
// 2.3.3
if (typeof x === 'object' || typeof x === 'function') {
try {
// 2.3.3.1
then = x.then
if (typeof then === 'function') {
// 2.3.3.2
then.call(x, function resolvePromise(y) {
// 2.3.3.3.3
if (thenCalled) return
thenCalled = true
// 2.3.3.3.1
return promiseResolution(promise2, y, resolve, reject)
}, function rejectPromise(r) {
// 2.3.3.3.3
if (thenCalled) return
thenCalled = true
// 2.3.3.3.2
return reject(r)
})
} else {
// 2.3.3.4
resolve(x)
}
} catch(e) {
// 2.3.3.3.4.1
if (thenCalled) return
thenCalled = true
// 2.3.3.2
reject(e)
}
} else {
// 2.3.4
resolve(x)
}
}
4.思考
以上,基本實現(xiàn)了一個簡易版的 promise ,說白了,就是對 Promises/A+ 規(guī)范的一個翻譯,將規(guī)范翻譯成代碼。因為大家的實現(xiàn)都是基于這個規(guī)范,所以不同的 promise 實現(xiàn)之間能夠共存(不得不說制定規(guī)范的人才是最厲害的)
function doSomething () {
return new promise((resolve, reject) => {
setTimeout(() => {
resolve('promise done')
}, 2000)
})
}
function doSomethingElse () {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('ES6 promise')
}, 1000)
})
}
this.promise2 = doSomething().then(doSomethingElse)
console.log(this.promise2)
ES7 的 Async/Await 也是基于 promise 來實現(xiàn)的,可以理解成 async 函數(shù)會隱式地返回一個 Promise , await 后面的執(zhí)行代碼放到 then 方法中
更深層次的思考,你需要理解規(guī)范中每一條制定的意義,比如為什么then方法不像jQuery那樣返回this而是要重新返回一個新的promise對象(如果then返回了this,那么promise2就和promise1的狀態(tài)同步,promise1狀態(tài)變更后,promise2就沒辦法接受后面異步操作進行的狀態(tài)變更)、 promise解決過程 中為什么要規(guī)定 promise2 和 x 不能指向同一對象(防止循環(huán)引用)。
5.極簡版promise
考慮到上述實現(xiàn)方法還是太全面,完全依照規(guī)范來寫的,不直觀且難懂。下面寫一個極簡版的實現(xiàn)方式,以便大家理解和在面試時使用:
function promise () {
this.status = 'pending' // 2.1
this.msg = '' // 存儲value與reason
let process = arguments[0],
that = this
process (function () {
that.status = 'resolve'
that.msg = argument[0]
}, function () {
that.status = 'reject'
that.msg = argument[0]
})
return this
}
promise.prototype.then = function () {
if (this.status === 'resolve') {
arguments[0](this.msg)
} else if (this.status === 'reject' && arguments[1]) {
arguments[1](this.msg)
}
}
四、promise的弊端
promise徹底解決了callback hell,但也存在以下一些問題
1.延時問題(涉及到evnet loop)
2.promise一旦創(chuàng)建,無法取消
3.pending狀態(tài)的時候,無法得知進展到哪一步(比如接口超時,可以借助race方法)
4.promise會吞掉內(nèi)部拋出的錯誤,不會反映到外部。如果最后一個then方法里出現(xiàn)錯誤,無法發(fā)現(xiàn)。(可以采取hack形式,在promise構(gòu)造函數(shù)中判斷onRejectedCb的數(shù)組長度,如果為0,就是沒有注冊回調(diào),這個時候就拋出錯誤,某些庫實現(xiàn)done方法,它不會返回一個promise對象,且在done()中未經(jīng)處理的異常不會被promise實例所捕獲)
5.then方法每次調(diào)用都會創(chuàng)建一個新的promise對象,一定程度上造成了內(nèi)存的浪費
五、總結(jié)
支持 promise 的庫有很多,現(xiàn)在主流的瀏覽器也都原生支持 promise 了,而且還有更好用的 Async/Await 。希望大家都有所收獲!