講給小白聽的Promise原理剖析

嫌疑犯X發表於2019-03-04

本文想從一個全新的角度來理解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就可以做到知其然並知其所以然了。

相關文章