Promise原理探究及實現
前言
作為ES6處理非同步操作的新規範,Promise一經出現就廣受歡迎。面試中也是如此,當然此時對前端的要求就不僅僅侷限會用這個階段了。下面就一起看下Promise相關的內容。
Promise用法及實現
在開始之前,還是簡單回顧下Promise是什麼以及怎麼用,直接上來談實現有點空中花園的感覺。(下面示例參考自阮大佬es6 Promis,)
定義
Promise 是非同步程式設計的一種解決方案,可以認為是一個物件,可以從中獲取非同步操作的資訊。以替代傳統的回撥事件。
常見用法
Promise的建立
es6規範中,Promise是個建構函式,所以建立如下:
const promise = new Promise((resolve, reject) => { setTimeout(resolve, 200, 'resolve'); // 可以為同步,如下操作 return resolve('resolve') })
注意resolve或者reject 一旦執行,後續的程式碼可以執行但就不會再更新狀態(否則這狀態回撥就無法控制了)。
舉個例子:
var a = new Promise((resolve,reject)=>{ resolve(1) console.log('執行程式碼,改變狀態') throw new Error('ss') }) a.then((res)=>{ console.log('resolved >>>',res) },(err)=>{ console.log('rejected>>>',err) }) // 輸出 // 執行程式碼,改變狀態 // resolved >>> 1
因此,狀態更新函式之後的再次改變狀態的操作都是無效的,例如異常之類的也不會被catch。
邏輯程式碼推薦在狀態更新之前執行。
建構函式
建構函式接收一個函式,該函式會同步執行,即我們的邏輯處理函式,何時執行對應的回撥,這部分邏輯還是要自己管理的。
至於如何執行回撥,就和入參有關係了。
兩個入參resolve和reject,分別更新不同狀態,以觸發對應處理函式。
觸發操作由Promise內部實現,我們只關注觸發時機即可
建構函式實現
那麼要實現一個Promise,其建構函式應該是這麼個樣子:
// 三種狀態 const STATUS = { PENDING: 'pending', RESOLVED:'resolved', REJECTED:'rejected' } class Promise{ constructor(fn){ // 初始化狀態 this.status = STATUS.PENDING // resolve事件佇列 this.resolves = [] // reject事件佇列 this.rejects = [] // resolve和reject是內部提供的,用以改變狀態。 const resovle = (val)=>{ // 顯然這裡應該是改變狀態觸發回撥 this.triggerResolve(val) } const reject = (val)=>{ // 顯然這裡應該是改變狀態觸發回撥 this.triggerReject(val) } // 執行fn try{ fn(resolve,reject) }catch(err){ // 執行異常要觸發reject,就需要在這裡catch了 this.triggerReject(err) } } then(){ } }
觸發回撥的triggerReject/triggerResolve 做的事情主要兩個:
- 更新當前狀態
- 執行回撥佇列中的事件
// 觸發 reject回撥
triggerReject(val){
// 儲存當前值,以供後面呼叫
this.value = val
// promise狀態一經變化就不再更新,所以對於非pending狀態,不再操作
if (this.status === STATUS.PENDING) {
// 更新狀態
this.status = STATUS.REJECTED
// 迴圈執行回撥佇列中事件
this.rejects.forEach((it) => {
it(val)
})
}
}
// resolve 功能類似
// 觸發 resolve回撥
triggerResolve(val) {
this.value = val
if(this.status === STATUS.PENDING){
this.status = STATUS.RESOLVED
this.resolves.forEach((it,i)=>{
it(val)
})
}
}
此時執行的話還是不能達到目的的,因為this.resolves/ this.rejects的回撥佇列裡面還是空呢。
下面就看如何會用then往回調佇列中增加監聽事件。
then用法
該方法為Promise例項上的方法,作用是為Promise例項增加狀態改變時的回撥函式。
接受兩個引數,resolve和reject即我們所謂成功和失敗回撥,其中reject可選
then方法返回的是一個新的例項(也就是新建了一個Promise例項),可實現鏈式呼叫。
new Promise((resolve, reject) => {
return resolve(1)
}).then(function(res) {
// ...
}).then(function(res) {
// ...
});
前面的結果為後邊then的引數,這樣可以實現次序呼叫。
若前面返回一個promise,則後面的then會依舊遵循promise的狀態變化機制進行呼叫。
then 實現
看起來也簡單,then是往事件佇列中push事件。那麼很容易得出下面的程式碼:
// 兩個入參函式
then(onResolved,onRejected){
const resolvehandle=(val)=>{
return onResolved(val)
},rejecthandle =(val)=>{
return onRejected(val)
}
// rejecthandle
this.resolves.push(resolvehandle)
this.rejects.push(rejecthandle)
}
此時執行示例程式碼,可以得到結果了。
new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'done');
}).then((res)=>{
console.log(res)
}) // done
不過這裡太簡陋了,而且then還有個特點是支援鏈式呼叫其實返回的也是promise 物件。
我們來改進一下。
then支援鏈式呼叫
then(onResolved,onRejected){
// 返回promise 保證鏈式呼叫,注意這裡每次then都新建了promise
return new Promise((resolve,reject)=>{
const resolvehandle = (val)=>{
// 對於值,回撥方法存在就直接執行,否則不變傳遞下去。
let res = onResolved ? onResolved(val) : val
if(Promise.isPromise(res)){
// 如果onResolved 是promise,那麼就增加then
return res.then((val)=>{
resolve(val)
})
}else {
// 更新狀態,執行完了,後面的隨便
return resolve(val)
}
},
rejecthandle = (val)=>{
var res = onRejected ? onRejected(val) : val;
if (Promise.isPromise(res)) {
res.then(function (val) {
reject(val);
})
} else {
reject(val);
}
}
// 正常加入佇列
this.resolves.push(resolvehandle)
this.rejects.push(rejecthandle)
})
}
此時鏈式呼叫和promise 的回撥也已經支援了,可以用如下程式碼測試。
new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'done');
}).then((res)=>{
return new Promise((resolve)=>{
console.log(res)
setTimeout(resolve, 200, 'done2');
})
}).then((res)=>{
console.log('second then>>', res)
})
同步resolve的實現
不過此時對於同步的執行,還是有些問題。
因為then中的實現,只是將回調事件假如回撥佇列。
對於同步的狀態,then執行在建構函式之後,
此時事件佇列為空,而狀態已經為resolved,
所以這種狀態下需要加個判斷,如果非pending狀態直接執行回撥。
then(onResolved,onRejected){
/**省略**/
// 剛執行then 狀態就更新,那麼直接執行回撥
if(this.status === STATUS.RESOLVED){
return resolvehandle(this.value)
}
if (this.status === STATUS.REJECTED){
return rejecthandle(this.value)
}
})
}
這樣就能解決同步執行的問題。
new Promise((resolve, reject) => {
resolve('done')
}).then((res)=>{
console.log(res)
})
// done
catch
catch方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。
直接看例子比較簡單:
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
此時catch是是getJSON和第一個then執行時的異常,如果只是在then中指定reject函式,那麼then中執行的異常無法捕獲。
因為then返回了一個新的promise,同級的reject回撥,不會被觸發。
舉個例子:
var a = new Promise((resolve,reject)=>{
resolve(1)
})
a.then((res)=>{
console.log(res)
throw new Error('then')
},(err)=>{
console.log('catch err>>>',err) // 不能catch
})
該catch只能捕獲建構函式中的異常,對於then中的error就不能捕獲了。
var a = new Promise((resolve,reject)=>{
resolve(1)
})
a.then((res)=>{
console.log(res)
throw new Error('then')
}).catch((err)=>{
console.log('catch err>>>',err) // catch err>>> Error: then at <anonymous>:6:11
})
推薦每個then之後都跟catch來捕獲所有異常。
catch 的實現
基於catch方法是.then(null, rejection)或.then(undefined, rejection)的別名這句話,其實實現就比較簡單了。
其內部實現呼叫then就可以了。
catch(onRejected){
return this.then(null, onRejected)
}
Promise.resolve/Promise.reject
該方法為獲取一個指定狀態的Promise物件的快捷操作。
直接看例子比較清晰:
Promise.resolve(1);
// 等價於
new Promise((resolve) => resolve(1));
Promise.reject(1);
// 等價於
new Promise((resolve,reject) => reject(1));
既然是Promise的自身屬性,那麼可以用es6的static來實現:
Promise.reject與其類似,就不再實現了。
// 轉為promise resolve 狀態
static resolve(obj){
if (Promise.isPromise(obj)) {
return obj;
}
// 非promise 轉為promise
return new Promise(function (resolve, reject) {
resolve(obj);
})
}
結束語
參考文章
阮一峰es6入門
https://promisesaplus.com/
http://liubin.org/promises-book/#chapter1-what-is-promise
本想把常見的promise面試題一起加上的,後面就寫成了promise的實現,手動Promise都可以實現的話,相關面試題應該問題不大。這裡附一個JavaScript | Promises interiew 大家可以看看。完整程式碼請