手把手教你實現一個完整的 Promise

weixin_34088583發表於2016-11-15

用過 Promise,但是總是有點似懂非懂的感覺,也看過很多文章,還是搞不懂 Promise的 實現原理,後面自己邊看文章,邊除錯程式碼,終於慢慢的有感覺了,下面就按自己的理解來實現一個 Promise。

 

已將每一步的程式碼都放在了 github 上,方便大家閱讀。如果覺得好的話,歡迎star。

想要完全理解程式碼,需要理解 this 和閉包的含義

 

Promise是什麼

簡單來說,Promise 主要就是為了解決非同步回撥的問題。用 Promise 來處理非同步回撥使得程式碼層次清晰,便於理解,且更加容易維護。其主流規範目前主要是 Promises/A+ 。對於 Promise 用法不熟悉的,可以參看我的這篇文章——es6學習筆記5--promise,理解了再來看這篇文章,會對你有很大幫助的。

在開始前,我們先寫一個 promise 應用場景來體會下 promise 的作用。目前谷歌和火狐已經支援 es6 的 promise。我們採用 setTimeout 來模擬非同步的執行,具體程式碼如下:

function fn1(resolve, reject) {
    setTimeout(function() {
        console.log('步驟一:執行');
        resolve('1');
    },500);
}

function fn2(resolve, reject) {
    setTimeout(function() {
        console.log('步驟二:執行');
        resolve('2');
    },100);
}

new Promise(fn1).then(function(val){
    console.log(val);
    return new Promise(fn2);
}).then(function(val){
    console.log(val);
    return 33;
}).then(function(val){
    console.log(val);
});

最終我們寫的promise同樣可以實現這個功能。

初步構建

下面我們來寫一個簡單的 promsie。Promise 的引數是函式 fn,把內部定義 resolve 方法作為引數傳到 fn 中,呼叫 fn。當非同步操作成功後會呼叫 resolve 方法,然後就會執行 then 中註冊的回撥函式。

function Promise(fn){
  //需要一個成功時的回撥
  var callback;
  //一個例項的方法,用來註冊非同步事件
  this.then = function(done){
    callback = done;
  }
  function resolve(){
    callback();
  }
  fn(resolve);
}

加入鏈式支援

下面加入鏈式,成功回撥的方法就得變成陣列才能儲存。同時我們給 resolve 方法新增引數,這樣就不會輸出 undefined。

function Promise(fn) {
    var promise = this,
        value = null;
        promise._resolves = [];

    this.then = function (onFulfilled) {
        promise._resolves.push(onFulfilled);
        return this;
    };

    function resolve(value) {
        promise._resolves.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}
  • promise = this, 這樣我們不用擔心某個時刻 this 指向突然改變問題。

  • 呼叫 then 方法,將回撥放入 promise._resolves 佇列;

  • 建立 Promise 物件同時,呼叫其 fn, 並傳入 resolve 方法,當 fn 的非同步操作執行成功後,就會呼叫 resolve ,也就是執行promise._resolves列中的回撥;

  • resolve 方法 接收一個引數,即非同步操作返回的結果,方便傳值。

  • then方法中的 return this 實現了鏈式呼叫。

但是,目前的 Promise 還存在一些問題,如果我傳入的是一個不包含非同步操作的函式,resolve就會先於 then 執行,也就是說 promise._resolves 是一個空陣列。

為了解決這個問題,我們可以在 resolve 中新增 setTimeout,來將 resolve 中執行回撥的邏輯放置到 JS 任務佇列末尾。

    function resolve(value) {
        setTimeout(function() {
            promise._resolves.forEach(function (callback) {
                callback(value);
            });
        },0);
    }

引入狀態

剖析 Promise 之基礎篇 說 這裡存在一點問題: 如果 Promise 非同步操作已經成功,之後呼叫 then  註冊的回撥再也不會執行了,而這是不符合我們預期的。

對於這句話不是很理解,有知道的可以留言說下,最好能給例項說明下。但我個人覺得是,then 中的註冊的回撥都會在 resolve 執行之前就新增到陣列當中,不會存在不執行的情況啊。

接著上面的步伐,引入狀態:

function Promise(fn) {
    var promise = this,
        value = null;
        promise._resolves = [];
        promise._status = 'PENDING';

    this.then = function (onFulfilled) {
        if (promise._status === 'PENDING') {
            promise._resolves.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };


    function resolve(value) {
        setTimeout(function(){
            promise._status = "FULFILLED";
            promise._resolves.forEach(function (callback) {
                callback(value);
            })
        },0);
    }

    fn(resolve);
}

每個 Promise 存在三個互斥狀態:pending、fulfilled、rejected。Promise 物件的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和從 pending 變為 rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對 Promise 物件新增回撥函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。

加上非同步結果的傳遞

目前的寫法都沒有考慮非同步返回的結果的傳遞,我們來加上結果的傳遞:

    function resolve(value) {
        setTimeout(function(){
            promise._status = "FULFILLED";
            promise._resolves.forEach(function (callback) {
                value = callback(value);
            })
        },0);
    }

序列 Promise

序列 Promise 是指在當前 promise 達到 fulfilled 狀態後,即開始進行下一個 promise(後鄰 promise)。例如我們先用ajax從後臺獲取使用者的的資料,再根據該資料去獲取其他資料。

這裡我們主要對 then 方法進行改造:

    this.then = function (onFulfilled) {
        return new Promise(function(resolve) {
            function handle(value) {
                var ret = isFunction(onFulfilled) && onFulfilled(value) || value;
                resolve(ret);
            }
            if (promise._status === 'PENDING') {
                promise._resolves.push(handle);
            } else if(promise._status === FULFILLED){
                handle(value);
            }
        })
        
    };

then 方法該改變比較多啊,這裡我解釋下:

  • 注意的是,new Promise() 中匿名函式中的 promise (promise._resolves 中的 promise)指向的都是上一個 promise 物件, 而不是當前這個剛剛建立的。

  • 首先我們返回的是新的一個promise物件,因為是同型別,所以鏈式仍然可以實現。

  • 其次,我們新增了一個 handle 函式,handle 函式對上一個 promise 的 then 中回撥進行了處理,並且呼叫了當前的 promise 中的 resolve 方法。

  • 接著將 handle 函式新增到 上一個promise 的 promise._resolves 中,當非同步操作成功後就會執行 handle 函式,這樣就可以 執行 當前 promise 物件的回撥方法。我們的目的就達到了。

有些人在這裡可能會有點犯暈,有必要對執行過程分析一下,具體參看以下程式碼:

new Promise(fn1).then(fn2).then(fn3)})

fn1, fn2, fn3的函式具體可參看最前面的定義。

  1. 首先我們建立了一個 Promise 例項,這裡叫做 promise1;接著會執行 fn1(resolve);

  2. 但是 fn1 中有一個 setTimeout 函式,於是就會先跳過這一部分,執行後面的第一個 then 方法;

  3. then 返回一個新的物件 promise2,  promise2 物件的 resolve 方法和 then 方法的中回撥函式 fn2 都被封裝在 handle 中, 然後 handle 被新增到   promise1._resolves 陣列中。

  4. 接著執行第二個 then 方法,同樣返回一個新的物件 promise3, 包含 promise3 的 resolve 方法和 回撥函式 fn3 的 handle 方法被新增到 promise2._resolves 陣列中。

  5. 到此兩個 then 執行結束。 setTimeout 中的延遲時間一到,就會呼叫 promise1的 resolve方法。

  6. resolve 方法的執行,會呼叫 promise1._resolves 陣列中的回撥,之前我們新增的 handle 方法就會被執行; 也就是 fn2 和 promsie2 的 resolve 方法,都被呼叫了。

  7. 以此類推,fn3 會和 promise3 的 resolve 方法 一起執行,因為後面沒有 then 方法了,promise3._resolves 陣列是空的

  8. 至此所有回撥執行結束

 但這裡還存在一個問題,就是我們的 then 裡面函式不能對 Promise 物件進行處理。這裡我們需要再次對 then 進行修改,使其能夠處理 promise 物件。

this.then = function (onFulfilled) {
        return new Promise(function(resolve) {
            function handle(value) {
                var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value;
                if( ret && typeof ret ['then'] == 'function'){
                    ret.then(function(value){
                       resolve(value);
                    });
                } else {
                    resolve(ret);
                }
            }
            if (promise._status === 'PENDING') {
                promise._resolves.push(handle);
            } else if(promise._status === FULFILLED){
                handle(value);
            }
        })
        
    };

在 then 方法裡面,我們對 ret 進行了判斷,如果是一個 promise 物件,就會呼叫其 then 方法,形成一個巢狀,直到其不是promise物件為止。同時 在 then 方法中我們新增了呼叫 resolve 方法,這樣鏈式得以維持。

失敗處理

非同步操作不可能都成功,在非同步操作失敗時,標記其狀態為 rejected,並執行註冊的失敗回撥。

有了之前處理 fulfilled 狀態的經驗,支援錯誤處理變得很容易。毫無疑問的是,在註冊回撥、處理狀態變更上都要加入新的邏輯:

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function(resolve, reject) {
            function handle(value) {
                .......
            }
            function errback(reason){
                reason = isFunction(onRejected) && onRejected(reason) || reason;
                reject(reason);
            }
            if (promise._status === 'PENDING') {
                promise._resolves.push(handle);
                promise._rejects.push(errback);
            } else if(promise._status === 'FULFILLED'){
                handle(value);
            } else if(promise._status === 'REJECTED') {
              errback(promise._reason);
        }
        })
        
    };

    function reject(value) {
        setTimeout(function(){
            promise._status = "REJECTED";
            promise._rejects.forEach(function (callback) {
                promise._reason = callback( value);
            })
        },0);
    }

新增Promise.all方法

Promise.all 可以接收一個元素為 Promise 物件的陣列作為引數,當這個陣列裡面所有的 Promise 物件都變為 resolve 時,該方法才會返回。

具體程式碼如下:

Promise.all = function(promises){
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
      }
    // 返回一個promise 例項
return new Promise(function(resolve,reject){ var i = 0, result = [], len = promises.length,   count = len;
      // 每一個 promise 執行成功後,就會呼叫一次 resolve 函式
function resolver(index) {   return function(value) {    resolveAll(index, value);   };  } function rejecter(reason){ reject(reason); } function resolveAll(index,value){
       // 儲存每一個promise的引數 result[index]
= value;
       // 等於0 表明所有的promise 都已經執行完成,執行resolve函式
if( --count == 0){ resolve(result) } }       // 依次迴圈執行每個promise for (; i < len; i++) {
         // 若有一個失敗,就執行rejecter函式 promises[i].then(resolver(i),rejecter); } }); }

Promise.all會返回一個 Promise 例項,該例項直到引數中的所有的 promise 都執行成功,才會執行成功回撥,一個失敗就會執行失敗回撥。

日常開發中經常會遇到這樣的需求,在不同的介面請求資料然後拼合成自己所需的資料,通常這些介面之間沒有關聯(例如不需要前一個介面的資料作為後一個介面的引數),這個時候  Promise.all 方法就可以派上用場了。

新增Promise.race方法

該函式和 Promise.all 相類似,它同樣接收一個陣列,不同的是隻要該陣列中的任意一個 Promise 物件的狀態發生變化(無論是 resolve 還是 reject)該方法都會返回。我們只需要對 Promise.all 方法稍加修改就可以了。

Promise.race = function(promises){
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to race.');
    }
    return Promise(function(resolve,reject){
        var i = 0,
            len = promises.length;

        function resolver(value) {
            resolve(value);
        }

        function rejecter(reason){
            reject(reason);
        }

        for (; i < len; i++) {
            promises[i].then(resolver,rejecter);
        }
    });
}

程式碼中沒有類似一個 resolveAll 的函式,因為我們不需要等待所有的 promise 物件狀態都發生變化,只要一個就可以了。

新增其他API以及封裝函式

到這裡,Promise 的主要API都已經完成了,另外我們在新增一些比較常見的方法。也對一些可能出現的錯誤進行了處理,最後對其進行封裝。

完整的程式碼如下:

(function(window,undefined){

// resolve 和 reject 最終都會呼叫該函式
var final = function(status,value){
    var promise = this, fn, st;
        
    if(promise._status !== 'PENDING') return;
    
    // 所以的執行都是非同步呼叫,保證then是先執行的
    setTimeout(function(){
        promise._status = status;
        st = promise._status === 'FULFILLED'
        queue = promise[st ? '_resolves' : '_rejects'];

        while(fn = queue.shift()) {
            value = fn.call(promise, value) || value;
        }

        promise[st ? '_value' : '_reason'] = value;
        promise['_resolves'] = promise['_rejects'] = undefined;
    });
}


//引數是一個函式,內部提供兩個函式作為該函式的引數,分別是resolve 和 reject
var Promise = function(resolver){
    if (!(typeof resolver === 'function' ))
        throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
    //如果不是promise例項,就new一個
    if(!(this instanceof Promise)) return new Promise(resolver);

    var promise = this;
    promise._value;
    promise._reason;
    promise._status = 'PENDING';
    //儲存狀態
    promise._resolves = [];
    promise._rejects = [];
    
    //
    var resolve = function(value) {
        //由於apply引數是陣列
        final.apply(promise,['FULFILLED'].concat([value]));
    }

    var reject = function(reason){
        final.apply(promise,['REJECTED'].concat([reason]));
    }
    
    resolver(resolve,reject);
}

Promise.prototype.then = function(onFulfilled,onRejected){
    var promise = this;
    // 每次返回一個promise,保證是可thenable的
    return new Promise(function(resolve,reject){
        
        function handle(value) {
            // 這一步很關鍵,只有這樣才可以將值傳遞給下一個resolve
            var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value;

            //判斷是不是promise 物件
            if (ret && typeof ret ['then'] == 'function') {
                ret.then(function(value) {
                    resolve(value);
                }, function(reason) {
                    reject(reason);
                });
            } else {
                resolve(ret);
            }
        }

        function errback(reason){
            reason = typeof onRejected === 'function' && onRejected(reason) || reason;
            reject(reason);
        }

        if(promise._status === 'PENDING'){
            promise._resolves.push(handle);
            promise._rejects.push(errback);
        }else if(promise._status === FULFILLED){ // 狀態改變後的then操作,立刻執行
            callback(promise._value);
        }else if(promise._status === REJECTED){
            errback(promise._reason);
        }
    });
}

Promise.prototype.catch = function(onRejected){
    return this.then(undefined, onRejected)
}

Promise.prototype.delay = function(ms,value){
    return this.then(function(ori){
        return Promise.delay(ms,value || ori);
    })
}

Promise.delay = function(ms,value){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve(value);
            console.log('1');
        },ms);
    })
}

Promise.resolve = function(arg){
    return new Promise(function(resolve,reject){
        resolve(arg)
    })
}

Promise.reject = function(arg){
    return Promise(function(resolve,reject){
        reject(arg)
    })
}

Promise.all = function(promises){
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }
    return Promise(function(resolve,reject){
        var i = 0,
            result = [],
            len = promises.length,
            count = len
            
        //這裡與race中的函式相比,多了一層巢狀,要傳入index
        function resolver(index) {
          return function(value) {
            resolveAll(index, value);
          };
        }

        function rejecter(reason){
            reject(reason);
        }

        function resolveAll(index,value){
            result[index] = value;
            if( --count == 0){
                resolve(result)
            }
        }

        for (; i < len; i++) {
            promises[i].then(resolver(i),rejecter);
        }
    });
}

Promise.race = function(promises){
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to race.');
    }
    return Promise(function(resolve,reject){
        var i = 0,
            len = promises.length;

        function resolver(value) {
            resolve(value);
        }

        function rejecter(reason){
            reject(reason);
        }

        for (; i < len; i++) {
            promises[i].then(resolver,rejecter);
        }
    });
}

window.Promise = Promise;

})(window);
View Code

 

下載完整版程式碼,點選 github ,如果覺得好的話,歡迎star。

 

程式碼寫完了,總要寫幾個例項看看效果啊,具體看下面的測試程式碼:

var getData100 = function(){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve('100ms');
        },1000);
    });
}

var getData200 = function(){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve('200ms');
        },2000);
    });
}
var getData300 = function(){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            reject('reject');
        },3000);
    });
}

getData100().then(function(data){
    console.log(data);      // 100ms
    return getData200();
}).then(function(data){
    console.log(data);      // 200ms
    return getData300();
}).then(function(data){
    console.log(data);      
}, function(data){
    console.log(data);      // 'reject'
});

Promise.all([getData100(), getData200()]).then(function(data){
    console.log(data);      // [ "100ms", "200ms" ]
});

Promise.race([getData100(), getData200(), getData300()]).then(function(data){
    console.log(data);      // 100ms
});
Promise.resolve('resolve').then(function(data){
    console.log(data);      //'resolve'
})
Promise.reject('reject函式').then(function(data){
    console.log(data);
}, function(data){
    console.log(data);     //'reject函式'
})

 

參考文章:

1、教你一步一步實現一個Promise - 飛魚

2、剖析 Promise 之基礎篇

3、Promise簡單實現(正常思路版)

4、大白話講解Promise(一)

5、Javascript 中的神器——Promise

相關文章