前端非同步一直是個熱門話題,作為新一代非同步程式設計解決方案的Promise
,相比傳統非同步程式設計,不僅解決了恐怖的回撥地獄,在寫法上也是相當方便簡潔。如此牛*的功能讓人不禁地想一探究竟,在各大網路公司面試中,手寫Promise也是常見的筆試題了,就讓我們一起來簡單探討一下Promise的實現吧!
引子
什麼是Promise?
Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。
Promise簡單用法
// 定義一個Promise物件
let promise = new Promise((resolve,reject) => {
true&&resolve(true) || reject(false)
});
promise.then(res=>{
console.log(res)
}).catch(err=>{
throw err
})
複製程式碼
如此一個低階簡單的Promise就完成了。
正文
思考一下,需要實現哪些功能
- 定義一個Promise
- 在Promise內部,需要一個建構函式(constructor),建構函式有兩個引數:
resolve
和reject
,建構函式內部需要一個狀態變數state
(pending、resolved、rejected),一個非同步成功的回參res
- 既然是Promise那必須得有
then
方法,有兩個引數分別是onFulfilled
和onRejected
- 有了then,自然有
catch
、finally
等一系列。。。。。。
宣告一個Promise
既然是手寫,那麼我們直接將原來的Promise物件覆蓋,這裡採用ES6的class寫法。
宣告建構函式
- Promise存在三個狀態(state)pending、fulfilled、rejected
- pending(等待態)為初始態,並可以轉化為fulfilled(成功態)和rejected(失敗態)
- 不管成功還是失敗,最終得到的res或者reason都無法改變
class Promise {
// 建構函式
constructor(executor){
// 狀態控制state,預設值值為pending、成功resolved、失敗rejected
this.state = 'pending';
// 成功返回值
this.res = '';
// 失敗返回原因
this.reason = '';
// resolve方法
let resolve = (res) => {
// 如果狀態state為pending,將狀態變為resolved,並將返回值賦給res
this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
};
let reject = (reason) => {
// 如果狀態state為pending,將狀態變為rejected,並返回錯誤原因
this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
};
// 如果executor報錯的話,直接執行reject
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
}
複製程式碼
實現then方法
then(onFulfilled,onRejected){
// state為resolved,執行onFulfilled
this.state === 'resolved' && (()=>{
onFulfilled(this.res)
})();
// state為rejected,執行onRejected
this.state === 'rejected' && (()=>{
onRjected(this.reason)
})();
}
複製程式碼
牛刀小試
此時,一個最基本最簡單最低階的Promise就完成了,讓我們先測試一下結果,隨機一個數,如果大於0.5則成功,如果小於則失敗。
let promise = new Promise((resolve,reject)=>{
let random = Math.random();
if(random>0.5){
resolve(random)
} else{
reject(random)
}
});
promise.then(res=>{
console.log('成功:' + res)
},err=>{
console.log('失敗:' + err)
})
複製程式碼
重新整理控制檯:
看起來沒什麼毛病,不過如果你認為這就結束了?
還是這該死的非同步
有人問,Promise不就是解決非同步的,怎麼又說到非同步,這裡要說的非同步就是指定時器了,比如setTimeout
,為什麼這樣說,這就涉及到js的事件迴圈機制(eventLoop)了,這裡暫且不提。
先看一個例子:
let promise = new Promise((resolve,reject)=>{
let random = Math.random();
if(random>0.5){
setTimeout(() => {
resolve(random)
}, 1000);
} else{
setTimeout(() => {
reject(random)
}, 1000);
}
});
promise.then(res=>{
console.log('成功:' + res)
},err=>{
console.log('失敗:' + err)
})
複製程式碼
此時,不管怎麼重新整理,控制檯就是不列印結果,不管正確還是錯誤,這是為什麼呢?
首先我們定義了一個Promise物件的例項,在例項中定義了一個定時器,一秒後返回,當執行到定時器之後,定時器掛起,繼續執行下面的promise.then,而此時因為定時器的緣故,不管是resolve還是reject都沒有執行,也就是說state的狀態任然沒有改變,所以在then方法中,沒有對應的項,如果再次執行then方法,同理依然無法執行,那麼問題來了,如何解決因為定時器導致的狀態未變化和多次執行then方法呢?
改造建構函式
首先為了多次執行then方法,我們定義兩個空陣列,分別存放成功或者失敗方法,然後在then方法中,如果狀態依然是pending,則將成功或者失敗的函式存入對應的陣列,當定時器時間過了之後,在resolve或者reject中迴圈對應陣列並執行函式。
constructor(executor){
// 原先程式碼不變
// ......
// 新增陣列
// 成功列表
this.resolvedList = [];
// 失敗列表
this.rejectedList = [];
// 改造一下resolve和reject
let resolve = (res) => {
this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
this.state === 'resolved' && this.resolvedList.forEach(fn => {
fn();
})
};
// 失敗
let reject = (reason) => {
this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
this.state === 'rejected' && this.rejectedList.forEach(fn => {
fn();
})
};
}
複製程式碼
改造then方法
由於定時器的原因,造成先執行then,那麼在then中將對應函式存起:
then(onFulfilled,onRejected){
this.state === 'pending' && (() =>{
this.resolvedList.push(()=>{
console.log('resolvedList.push')
onFulfilled(this.res);
});
this.rejectedList.push(()=>{
onRejected(this.reason)
});
return
})()
// state為resolved,執行onFulfilled
this.state === 'resolved' && (()=>{
onFulfilled(this.res)
})();
// state為rejected,執行onRejected
this.state === 'rejected' && (()=>{
onRjected(this.reason)
})();
}
複製程式碼
呼叫then方法
promise.then(
res => {
console.log("成功1:" + res);
},
err => {
console.log("失敗1:" + err);
}
);
promise.then(
res => {
console.log("成功2:" + res);
},
err => {
console.log("失敗2:" + err);
}
);
複製程式碼
重新整理控制檯:
此時控制檯已經可以正常列印。
then方法的鏈式呼叫
為了解決回撥地獄,在Promise中,我們是這樣的寫法:new Promise().then().then()
,道理其實很簡單,第一個then方法返回的值也是一個Promise物件,那麼就能繼續使用then方法。
當我們在第一個then中return了一個引數(引數未知,需判斷)。這個return出來的新的promise就是onFulfilled()或onRejected()的值:
- 返回值是普通值如:數值、字串等,直接將其作為
callBackPromise
成功的結果 - 如果是一個Promise,則取其結果作為
callBackPromise
成功的結果
一個判斷返回值的函式callBackPromise
- res 不能是null
- 宣告瞭then,如果取then報錯,則走reject()
function callBackFunction(callBackPromise,res, resolve, reject){
// 判斷callBackPromise === res ,如果等於會造成死迴圈
if(callBackPromise === res){
return reject(new TypeError('注意死迴圈了!'));
}
// 防止重複呼叫
let reCall;
if(res !== null && (typeof res === 'Object' || typeof res === 'function')){
try {
// 鏈式,呼叫上一次結果的then
let then = res.then;
typeof then === 'function' && then.call(res, nextRes => {
if(reCall){
return
}
reCall = true;
callBackPromise(callBackPromise,nextRes,resolve,reject)
},err => {
if(reCall)
return;
reject(err);
});
resolve(res)
}catch(e){
if(reCall)
return;
reject(e)
}
}else{
reject(res)
// resolve(res)
}
}
複製程式碼
繼續改造then方法
then(onFulfilled,onRejected){
// 注意此處用到的var,如果用let會造成callBackFunction報錯,原因是let沒有變數提升
var callBackPromise = new Promise((resolve,reject) => {
this.state === 'pending' && (() =>{
this.resolvedList.push(()=>{
let res = onFulfilled(this.res);
callBackFunction(callBackPromise,res,resolve,reject)
});
this.rejectedList.push(()=>{
let res = onRejected(this.reason)
callBackFunction(callBackPromise,res,resolve,reject)
});
return
})()
this.state === 'resolved' && (()=>{
let res = onFulfilled(this.res);
callBackFunction(callBackPromise,res,resolve,reject)
})();
this.state === 'rejected' && (()=>{
let res = onRejected(this.reason);
callBackFunction(callBackPromise,res,resolve,reject)
})();
})
return callBackPromise;
}
複製程式碼
測試一下,如果列印為空多重新整理幾次,因為沒有實現catch方法:
promise.then(res => {
console.log('第一次回撥',res)
return '第一次返回'
})
.then(res2=>{
console.log('第二次回撥',res2)
return '第二次回撥'
})
.then(res3=>{
console.log('第三次回撥',res3)
})
複製程式碼
可以看到此時已經可以進行鏈式操作。
後記
到此,一個基本的鏈式呼叫已經完成,然而Promise的博大精深遠不及此,諸如onFulfilled,onRejected的可選性,catch、finally、all等方法的實現都還沒有完成,需要不斷完善,如果上述程式碼中有錯誤的還請各位大佬指出,輕噴。