本文想從一個全新的角度來理解Promise的實現原理,即通過Promise的一些外在表現,一步步的去實現一個符合Promise所有行為的Promisee。
雖然說是針對小白使用者的,但是如果對Promise的使用一無所知的話,恕老夫也無能為力了,還是先去學習一下Promise的用法吧。
下面我們根據promise的一些測試用例慢慢完善我們自己的Promise,希望讀者能開啟編輯器,跟著本文的步驟一起去敲一下程式碼。看看為了實現Promise的每個行為都做了哪些工作,這樣到最後Promise內部的運轉機制就盡在我們掌握了。我們姑且把我們們要實現的Promise叫做MyPromise吧。
首先我們新建一個MyPromise建構函式.一開始MyPromise是一個空函式。
function MyPromise(fn){
} 複製程式碼
這裡fn是在建立Promise例項時使用者傳給Promise的回撥函式,我們知道fn裡會有兩個引數resolve和reject。
0、測試fn同步執行
我們知道Promise接收的回撥fn是同步執行的,以下程式碼會先列印1,再列印2
//0 測試fn同步執行
var a = new Promise((resolve, reject) => {
console.log(1)
});
console.log(2)複製程式碼
那麼我們的MyPromise改為
function MyPromise(fn){
fn()
} 複製程式碼
這樣我們的MyPromise就具備了promise同步執行的行為。
剛才說了fn接收resolve和reject兩個引數,我們就在MyPromise內部定義兩個函式分別叫resolve和reject,而且把這倆當引數傳給fn呼叫。
Promise的例項有一個then引數,我們一塊給定義了。
function MyPromise(fn){
function resolve(){
}
function reject(){
}
fn(resolve,reject);
this.then=function(onFullfilled,onRejected){}
} 複製程式碼
1、測試resolve,reject,then
在resolve時會呼叫傳給then的第一個函式,即成功回撥,並把resolve的值傳給成功回撥。
在reject時會呼叫傳給then的第二個函式,即失敗回撥,並把reject的值傳給失敗回撥。
//1 測試resolve
var a = new Promise((resolve, reject) => {
resolve(`success`);
});
a.then((val) => {
console.log(val) //列印success
})
//2 測試reject
var b = new Promise((resolve, reject) => {
reject(`error`);
});
b.then((val) => {
console.log(val)
}, (error) => {
console.log(error) //列印error
})複製程式碼
我們定義一個陣列deffers來儲存then函式傳入的成功失敗回撥,並在resolve或者reject的時候有選擇性的執行其中一個。 這裡還宣告瞭value和status兩個變數,value代表當前Promise最終改變的值,是resolve的值或者reject的error。status代表Promise的狀態,最初是pending,用null表示。reoslve了改變為true,reject的話改為false。只能改變一次,無論轉成true還是false都不能再改變了。
function MyPromise(fn){
var value; //resolve或者reject的值,在resolve或者reject時改變
var status=null; //該Promise的狀態,null:初始, true:成功(resolve), false:失敗(reject)
var deffers=[] //回撥陣列,每呼叫一次then,
//就往裡push一個{onFullFilled:onFullFilled,onRejected:onRejected}的回撥對。
//並在resolve或者reject時遍歷呼叫每一個onFullFilled或者onRejected函式
fn(resolve,reject);
function resolve(val){
value=val;
status=true;
final()
}
function reject(val){
value=val;
status=false;
final()
}
//遍歷執行deffers裡的函式
function final(){
for(var i=0,len=deffers.length;i<len;i++){
handle(deffers[i]) //deffers[i]=>{onFullFilled:onFullFilled,onRejected:onRejected}
}
}
//真正的處理結果的函式,根據status的值來判斷
function handle(deffer){ //deffer=>{onFullfilled:onFullfilled,onRejected:onRejected}
if(status===true){
deffer.onFullfilled && deffer.onFullfilled(value)
}
if(status===false){
deffer.onRejected && deffer.onRejected(value)
}
}
this.then=function(onFullfilled,onRejected){
var deffer = {onFullfilled:onFullfilled,onRejected:onRejected}
deffers.push(deffer);
handle(deffer);
}
} 複製程式碼
2 測試非同步的resolve和非同步的reject
//3 測試非同步的resolve
var b = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`success`);
}, 1000);
});
b.then((val) => {
console.log(val, ` async`) //1s後列印 success async
}, (error) => {
console.log(error, ` async`)
})
//4 測試非同步的reject
var b = new Promise((resolve, reject) => {
setTimeout(() => {
reject(`error`);
}, 1000);
});
b.then((val) => {
console.log(val, ` async`)
}, (error) => {
console.log(error, ` async`) //1s後列印 error async
})複製程式碼
這部分的程式碼邏輯上一步已經實現了,不再贅述
3 測試resolve和reject都呼叫的情況
即使一個Promise的resolve和reject都呼叫,Promise的狀態會根據呼叫順序只改變一次,後邊的resolve或者reject是無效的。
//5 測試resolve 和 reject都會呼叫的情況 先呼叫resolve
var b = new Promise((resolve, reject) => {
resolve("success");
reject(`error`);
});
b.then((val) => {
console.log(val, `同時呼叫`) //列印 success 同時呼叫
}, (error) => {
console.log(error, `同時呼叫`)
})
//5 測試resolve 和 reject都會呼叫的情況 先呼叫reject
var b = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("success");
},100)
reject(`error`);
});
b.then((val) => {
console.log(val, `同時呼叫`)
}, (error) => {
console.log(error, `同時呼叫`) //列印 error 同時呼叫
})複製程式碼
我們的MyPromise實現如下,只貼修改的部分
function MyPromise(fn){
...
//fn(resolve,reject);
doResolve(fn,resolve,reject) //不呼叫fn,改為呼叫doResolve,doResolve裡呼叫fn
function doResolve(fn,resolve,reject){
var hasChange =false;
fn((value)=>{
if(hasChange){
return;
}
hasChange=true; //開關,呼叫一次resolve或者reject之後就設定為true,
//下次就不會進來啦。下面同理
resolve(value)
},(error)=>{
if(hasChange){
return;
}
hasChange=true;
reject(value)
})
}
} 複製程式碼
4 測試同一個promise呼叫多個then
Promise可以呼叫多次then,就像上文所說,這些回撥都被放入了Promise內的deffers陣列,並在resolve或者reject時遍歷deffers依次呼叫,這部分的邏輯上文已經完成。
//6 測試同一個promise呼叫多個then
var b = new Promise((resolve, reject) => {
resolve(`success`)
});
b.then((val) => {
console.log(val, ` then1`)
}, (error) => {
console.log(error, ` then1`)
})
b.then((val) => {
console.log(val, ` then2`)
}, (error) => {
console.log(error, ` then2`)
})複製程式碼
5 測試連續呼叫then的情況
Promise呼叫then方法的返回值是一個新的Promise,這個新Promise會接收前一個Promise的then回撥裡的成功回撥返回值呼叫resolve,或者失敗回撥返回值呼叫reject,因此
var b = new Promise((resolve, reject) => {
resolve(`success`)
});
var newP = b
.then((val) => {
console.log(val, ` then1`);
return `進入第二個then的成功回撥` //如果b被resolve了,newP就會resolve 這個回撥的返回值
}, (error) => {
console.log(error, ` then1`);
return `進入第二個then的失敗回撥` //如果b被reject了,newP就會reject 這個回撥的返回值
});
newP.then((val) => {
console.log(val, ` then2`);
}, (error) => {
console.log(error, ` then2`);
})複製程式碼
這裡我們需要修改一下then方法
function MyPromise(fn){
...
this.then=function(onFullfilled,onRejected){
return new MyPromise((resolve,reject)=>{
let deffer = {onFullfilled:onFullfilled,onRejected:onRejected,resolve:resolve,reject:reject};
deffers.push(deffer)
handle(deffer)
})
}
}複製程式碼
這裡有幾處修改,第一點是讓then方法返回一個新的MyPromise,第二個是deffers陣列裡的每一個物件都快取了新MyPromise的resolve和reject,這樣前一個MyPromise的成功回撥執行完了我們可以取出新MyPromise的resolve執行,這樣就會形成一個管道了。reject同理。
下面我們來處理MyPromise鏈式呼叫的邏輯,主要集中在handle函式裡
function MyPromise(fn){
...
//真正的處理結果的函式,根據status的值來判斷
function handle(deffer){ //deffer=>{onFullfilled:onFullfilled,onRejected:onRejected,resolve:resolve,reject:reject} 這裡的deffers快取了新Promise的resolve和reject
//cb是處理函式,根據status來取
var cb = status===true?deffer.onFullfilled : deffer.onRejected;
//如果沒有傳處理函式,比如resolve卻沒有傳成功回撥,我們就執行新Promise的resolve,把成功結果往後傳遞.
if(cb==null){
status==true? deffer.resolve(value):deffer.reject(value);
return;
}
//下面是有cb的邏輯
var val;
//這裡用try catch的原因是成功和失敗回撥是使用者定義的,執行可能會報錯,一旦報錯就進入下個Promise的reject流程
try{
val=cb(value);
}catch(e){
deffer.reject(e);
return ;
}
//這裡只要成功拿到val值,不論cb是當前Promise的成功處理函式,還是失敗處理函式,只要當前Promise處理了,就進入下個Promise的resolve流程
deffer.resolve(val)
}
}複製程式碼
到這裡,結合then方法和handle方法,MyPromise可以鏈式呼叫的邏輯就處理完了。handle裡處理了then呼叫時不傳回撥的情況,也處理了傳的回撥函式執行報錯的情況。所以以下測試是通過的
// 連續呼叫then
var b = new MyPromise((resolve, reject) => {
resolve(`success`)
});
b.then(value=>{
console.log(value,` then1 連續呼叫then`); //這裡會列印,value=`success`
return `new data`
}, (error) => {
console.log(error, ` then1`)
}).then(value=>{
console.log(value) // 這裡會列印,value=`new data`
})
// 測試then裡不寫某個回撥
var b = new MyPromise((resolve, reject) => {
resolve(`success`)
});
b.then(null, (error) => {
console.log(error, ` then1`)
}).then((val) => {
console.log(val, ` then2 then1沒有成功回撥`) //這裡會列印,因為前一個MyPromise沒有傳成功回撥,就會進入這裡
}, (error) => {
console.log(error, ` then2 then1沒有成功回撥`)
});
var b = new MyPromise((resolve, reject) => {
reject(`error`)
});
b.then((value) => {
console.log(value, ` then1`)
}, null).then((val) => {
console.log(val, ` then2 then1沒有失敗回撥`)
}, (error) => {
console.log(error, ` then2 then1沒有失敗回撥`) //這裡會列印,因為前一個MyPromise沒有傳失敗回撥,就會進入這裡
});
//測試回撥裡報錯
var b = new MyPromise((resolve, reject) => {
resolve(`error`)
});
b.then((value) => {
throw "拋個錯誤";
return 333;
}).then((val) => {
console.log(val, ` then2 then1執行報錯`)
}, (error) => {
console.log(error, ` then2 then1執行報錯`) //這裡會列印,因為then1裡的成功回撥執行時報錯了
});複製程式碼
到這裡我們關於Promise的邏輯基本就完成了。等等,還有種情況,有時我們會resolve一個新的Promise,或者在then的回撥裡又返回一個新的Promise,這個該怎麼解決呢?
6 測試resolve一個Promise或者then的回撥返回新Promise的情況
下面我們就來處理這個問題,這部分邏輯主要在MyPromise內部定義的resolve方法裡,這裡的resolve的引數value有可能是個新的Promise
function MyPromise(fn){
...
function resolve(val){
//這裡用鴨式判別法來斷定val是個Promise物件,只要有then方法就可以了
if (val && (typeof val === "object" || typeof val === "function")) {
var then = val.then;
if (typeof then === "function") {
//如果val是個Promise物件,就把當前Promise的resolve和reject當做回撥傳給val的then,
//等val本身狀態變更時自會呼叫resolve和reject,這樣就能拿到val最終要傳給我們的值啦
doResolve(then.bind(val), resolve, reject);
return;
}
}
//走到這裡說明val已經是個非Promise物件了
value=val;
status=true;
final()
}
}複製程式碼
以上就是對Promise物件的解析,當然還有一些細節還需要完善。如果能看到這裡並且看明白了,以後再用Promise就可以做到知其然並知其所以然了。