Promise 原始碼解析

tanka發表於2019-05-07

寫promise的時候,發現自己應用了樹,連結串列,前度遍歷等方法,覺得對於部分想了解promise機制,並且加深資料結構學習的同學有些許幫助,決定寫一篇文章分享出來。不過文筆實在堪憂,沒有耐心看下去的同學,可以直接看原始碼。原始碼地址 .

promise的狀態是什麼時候變化的 ?

1:通過new Promise()生成的promise的狀態變化過程:
當呼叫resolve時:

    情況1:
    promise1 = new Promise((resolve) => {
        setTimeout(() => {
            resolve('promise1');
        }, 1000)
    })
複製程式碼

情況1: 當引數是一個非promise的時候,1秒後promise的狀態立即變成resolve,並執行then裡面的事件.

    情況2: 
    promise1 = new Promise((resolve) => {
        setTimeout(() => {
            promise2 = new Promise((resolve, reject) => {
                resolve('promise2');
            })
            resolve(promise2);
        }, 1000)
    })
複製程式碼

情況2: 當引數是另一個promise的時候,這時promise1的狀態由promise2來決定,什麼時候promise2變化了狀態,promise1的狀態也會相應的變化,並且狀態保持一致.

當呼叫reject時:

這裡與resolve不同的是,reject不管引數是什麼,狀態都會立即變為reject。

2:通過then()或者catch()生成的promise的狀態變化過程

    情況1:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        return 'promise2';
    })
複製程式碼

情況1: 當回撥函式裡面直接return一個非promise,和上面的情況1一樣,當前的promise2狀態變為resolve。相當於執行了(resolve('非promise'))

    情況2:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        promise3 = new Promise((resolve, reject) => {
            resolve('promise3');
        })
        return promise3;
    })
複製程式碼

情況2: 當回撥函式裡面直接return一個promise3,和上面情況2一樣,當前promise2的狀態依賴於primise3,相當於執行了(resolve(promise3))

    情況3:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        console.log( iamnotundefined );
    })
複製程式碼

情況3: 當回撥函式裡面程式碼報錯了,並且沒有被catch到的,當前promise狀態變為reject.(非同步的error程式碼catch不到,不會影響promise狀 態變化)

通過幾個promise例子說明一下

promise1 = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve('promise1_resolve_data');
    },1000)
})
console.log(promise1);
promise2 = promise1.then((data) => {
    console.log('promise2---', data);
    return 'promise2';
})
console.log(promise2);
promise3 = promise1.then((data) => {
    console.log('promise3---', data);
    return 'promise3';
})
console.log(promise3);
setTimeout(() => {
    console.log('--promise1--', promise1);
    console.log('--promise2--', promise2);
    console.log('--promise3--', promise3);
},3000)
複製程式碼

程式碼執行結果:

依次輸出promise1,promise2,promise3,狀態都是pendding.一秒過後執行relove,promise1狀態變為resolve,值為'promise1_resolve_data'.之後依次執行promise1.then裡面的回撥函式,promise2狀態變為resolve,值為'promise2'.promise3狀態變為resolve,值為'promise3'.

上面程式碼段看出了什麼?

1:當初始化promise1,promise2,promise3後,三個promise的狀態都是pendding.
2:當promise1裡面的resolve執行後,promise1的狀態立即變為resolve,值為resolve函式引數.
3:promise2,promise3都是通過promise1的then方法生成出來的,並且在promose1狀態變為resolve之後也都依次狀態變為了resolve。

通過上面的程式碼可以先得出的結論是:

(1) : 每一個promise的狀態的變化都不是立即就變化得,而是在未來的某一個時刻變化的。這裡可以想到:當我們自己實現的時候,一定要有一個結構去維護著所有promise.

(2) : 什麼結構呢? 這裡可以看出,promise2,promise3都是由promise1的then方法返回的,可以看出這是一個一對多的關係結構,所以這裡的結構一定是一個樹的結構。

(3) : 什麼時候去'裝載'每一個promise和相關的事件呢?很簡單,then和catch方法裡面。

(4) : 什麼時候去'執行'promise狀態變化,相關的事件回撥? resolve,reject裡面。

(5) : 說白了,也就是兩個過程,裝載過程(then,catch),執行過程(resolve,reject)

開始寫程式碼

當去實現一個東東的時候,比如promise,首先要做的是熟悉promise的語法,特性。分析每一個promise之間的關係,然後才能確定一個合適的資料結構去儲存它。前期的結構關係設計合理了,程式碼寫起來也會很容易。

    只寫核心程式碼
    function PP(){
        let promises = new Map(); // 儲存所有的promise例項
        let index = 1;
        // Promise 建構函式
        function App(fn){
            this.state = "pendding";
            this.id = +index; //每個promise的唯一標識
            fn(this.resolve.bind(this), this.reject.bind(this));
        }
        return App;
    }
    程式碼很簡單,不做解釋
複製程式碼

前面說到了,promise的實現其實就是兩個過程,裝載執行. 先說下裝載過程,也就是then() catch()的實現

    只寫核心程式碼
    App.prototype.then = function(resolve, reject){
        let instance = new App(()=>{}); // 生成一個初始狀態的promise,並返回
        //把instance和相應的回撥儲存起來 
        /**
         * type: 用來判定這個promise是通過then方法建立的
         * instance: promise例項
         * callback: 儲存的事件 
         */
        let item = {
            type : 'then', 
            instance : instance,
            callback : length > 1 ? ([{
                status : 'resolve',
                fn : resolveFn
            },{
                status : 'reject',
                fn : rejectFn
            }]) : ([{
                status : 'resolve',
                fn : resolveFn
            }])
        }
        // 這裡通過map儲存的,兩個promise之間的關係就通過promise的_id相互關聯.
        let p_item;
        if(p_item = promises.get(this._id)){
            p_item.push(item);
        }else{
            promises.set(this._id,[item])
        }
        return instance;
    }
    
    App.prototype.catch = function(rejectFn){
        // 和then差不多
        let instance = new app(()=>{});
        let item = {
            type : 'catch',
            instance : instance,
            callback : ([{
                status : 'reject',
                fn : rejectFn
            }])
        }
        let p_item;
        if(p_item=promises.get(this._id)){
            p_item.push(item);
        }else{
            promises.set(this._id,[item])
        }
        return instance;
    }
複製程式碼

說下執行的過程 , resolve() , reject()的實現 。
輔助案例: 程式碼 2-1

    promise1 = new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("resolve data from promise1");
        },1000)
    })
    promise2 = promise1.then((data) => {
        console.log('promise2---', data);
        return 'promise2';
    })
    promise3 = promise2.then((data) => {
        console.log('promise3---', data);
        return 'promise3';
    })
    promise4 = promise1.then((data) => {
        console.log('promise4---', data);
        return 'promise2';
    })
    promise5 = promise1.catch((data) => {
        console.log('promise4---', data);
        return 'promise2';
    })
複製程式碼

Promise 原始碼解析

這是程式碼2-1,promise之間的關係圖

執行過程:一秒後執行了resolve方法,當前promise狀態變為resolve.之後拿出與promise1下面的三個promise,分別是promise2,promise4,promise5.之後拿出每一個promise相關的事件,並執行。上面說了,像promise2,promise4,promise5這些通過then或者catch生成的promise,狀態變化過程由返回值來決定。

    App.prototype.resolve = function(data){
        let ans = null; // 回撥函式的結果
        let promise = null; //每一個子節點中的promise例項
        let items; //一個節點下面的子節點
        //執行mic任務佇列裡面的任務 , 這裡用setTimeout(fn,0)代替
        setTimeout(() => {
            // 上面說到,這裡做的事就是處理promise的變化。
            if(typeof data == 'object' && data!==null &&  data.__proto__.constructor == app){
                // 如果傳入的引數是一個promise物件,這個時候當前的promise的狀態不是立即變化的,而是依賴於傳入的promise也就是data的變化而變化。
                // 所以這裡要做的是就是關聯這兩個promise,這裡我用的連結串列
                data.previous = this;
            }else{
              // 這裡也就是上面說的情況1,resolve傳入的引數是一個非promise,這個時候當前promise立即變化,並執行相關的事件回撥.
              setTimeout(() => {
                this.state = "resolve";
                this.value = data;
                loadLineToList(this); // (很重要,單獨解釋2)
                //拿出當前節點下面的所有子節點
                if(items = promise.get(this._id)){
                    // 這裡以2-1示例程式碼為例,分別拿出promise2,promise4,promise5 .
                    // 上面promise項裡面的資料結構,分別是 type欄位,instance欄位,callback欄位。在then,或者catch裡面有寫?
                    // 拿出每一個promise的callback,並執行
                    for(let i=0;i<items.length;i++){
                        if(items[i].type == 'then'){
                            try{
                                ans = items[i].callback[0].fn(data);
                            }catch(err){
                                promise = promises.get(this._id)[i].instance;
                                promise.reject(err);
                                continue;
                            }  
                        }
                        //這裡已經拿到了事件執行的結果,ans 
                        if(typeof ans == 'object' && ans!==null &&  ans.__proto__.constructor == app){
                            ans.previous = promise;
                        }else{
                            if(promise){
                                promise.resolve(ans);
                            }
                        }
                    }
                }else{
                    //下面沒有節點了,出口
                    return;
                }
              },0)
            }
        },0)
    }
複製程式碼

程式碼2-2,返回值都是非promise,處理過程如上。接下來說另一種情況,返回值是promise , loadLineToList()這個函式就是用來處理這種情況的

    promise1 = new Promise((resolve, reject) => {
        setTimeout(()=>{
            promise2 = new Promise((resolve) => {
                setTimeout(() => {
                    promise5 = new Promise((resolve) => {
                        resolve('promise5');
                    })
                    promise7 = promise5.then(() => {
                        
                    })
                    resolve(promise5);
                },1000)
            })
            console.log('1s');
            resolve(promise2);
        },1000)
    })
    promise3 = promise1.then((data) => {
        console.log(data);
        promise4 = new Promise((resolve) => {
            setTimeout(() => {
                resolve('promise4');
            },1000)
        })
        return promise4;
    })
    promise6 = promise3.then((data) => {
        console.log(data);
    })
    setTimeout(() => {
        console.log('--promise1--', promise1);
        console.log('--promise2--', promise2);
        console.log('--promise3--', promise3);
        console.log('--promise4--', promise4);
        console.log('--promise5--', promise5);
        console.log('--promise6--', promise6);
    },4000)
複製程式碼

Promise 原始碼解析
上面是程式碼2-2,promise關係圖,分析出每一個promise之間的關係是重要的,只有明確了關係才能設計出合適的資料結構。

上面的程式碼說明了引數是promise的情況 , promise1的變化依賴於promise2, promise2的狀態依賴於promise5. 同樣的,promise3的狀態依賴於promise4. 這裡可以清晰的看出,promise之間的關係是單向的,1對1的,所以用連結串列是合適的。

App.prototype.then程式碼中的data.previous = this;ans.previous = promise;用來建立連結串列的。loadLineToList這個函式用來處理連結串列中promise之前的關係。保持promise1,promise2,promise5狀態一致,並且把promise2,promise5下面的所有promise'移'到promise1的下面。

reject的實現

說reject實現之前,先說明下promise的catch機制。

    promise1 = new Promise((resolve, reject) => {
        reject('promise1');
    })
    promise2 = promise1.then(() => {
    
    });
    promise4 = promise1.then(() => {
    
    });
    promise3 = promise2.catch(() => {
    
    })
複製程式碼

上面程式碼會報一個 Uncaught Error: (in promise) promise1,如果沒有最後的promise3的catch,會報2個Uncaught Error: (in promise) promise1

Promise 原始碼解析

promise之間的關係是樹形的,當一個節點狀態變成了reject,那麼一定要在此節點的下面一條線路上,有一個節點去catch這個reject,不然就會報錯。像上面的promise1變成了reject,會向下面的子節點去'發散',promise2沒有catch,那麼promise2的狀態變成reject,並且繼續向下找,promise3catch到了,然後結束。另一條線路,promise4沒有catch到,狀態變為reject,由於下面沒有節點了,也就是沒有catch,所以會抱一個Uncaught Error: (in promise) promise1

說清了catch機制,再去寫reject相關的程式碼就容易了。

    App.prototype.reject = function(error){
        let promise = null; //子節點
        let fn = null;  //then or catch的回撥函式
        setTimeout(() => {
            this.state = "reject";
            this.value = error;
            loadLineToList(this);
            let list = promises.get(this._id);//拿出當前節點下面的所有子節點
            //出口,沒有找到,報錯
            if(!list || list.length==0){
                throw new Error("(in promise) "+error);
            }
            for(let i=0;i<list.length;i++){
                promise = list[i].instance; // 從左的第一個子節點開始
                type = list[i].type;
                if(type == 'then'){   // 這個promise 是通過p1.then() 出來的 , 但是由於p1是reject , 所以當前promise轉換成reject
                    //處理then有兩個回撥函式的情況,第一個回撥函式相當於catch
                    if(list[i].callback.length == 1){
                        promise.value = error;
                        promise.reject(error);
                        continue;
                    }else{
                        fn = list[i].callback[1].fn;
                    }
                }
                // 拿到catch裡面的fn
                if(!fn){
                    fn = list[i].callback[0].fn; 
                }
                
                let ans = null; // 回撥函式的結果
                // catch回撥函式裡的程式碼,如果程式碼報錯,當前promise變為reject
                try{
                    ans = fn(error);
                    fn = null;
                }catch(err){
                    promise.reject(err);
                    continue;
                }
                promise.value = ans;
                if(typeof ans == 'object' && ans!==null &&  ans.__proto__.constructor == App){
                    ans.previous = promise;
                }else{
                    if(promise){
                        promise.resolve(ans);
                    }
                }
            }
        }, 5)
    }
複製程式碼

總結

其實看promise的實現,每一個promise之間的關係是通過樹的結構相互聯絡的。實現也是分為兩個過程,裝載和執行。裝載也就是構建樹的過程,catch和then方法。執行就是通過resolve和reject方法前度遍歷去找出下面的節點,改變每一個promise的狀態,並執行相關的回撥函式。

相關文章