前言
作為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 大家可以看看。完整代碼請戳