JavaScript深入淺出非同步程式設計二、promise原理

搬磚的碼農發表於2019-01-10

其實Promise本身並不具備非同步的能力,而之所以這裡需要單獨開一篇說明其原理,是因為Promise非同步程式設計的過程中是一個不可或缺的一環。原因下面細說。

在說promise之前,有必要先說下JS中的回撥方式。比如下面:

function doSomethingAfterTime(time, something) {
    setTimeout(fun, time);
}
複製程式碼

但是這樣的回撥方式有一個問題,可讀性太差。另外當回撥的層次多了以後,容易陷入回撥地獄。舉個例子:

function func1(cb){
    // do something
    cb();
}
function func2(cb){
    // do something
    cb();
}
function func3(cb){
    // do something
    cb();
}
// do
func1(function(){
    func2(function(){
        func3(function(){

        });
    });
});
複製程式碼

這樣的程式碼讀起來簡直就是折磨,暈死!

下面試著改進下程式碼,試著將回撥函式封裝起來。順便剖析下promise的原理。

Promise的原理

先來一個最簡單的。

function Promise(something){
    var callback = null;
    this.then = function(onCompelete){
        callback = onCompelete;
    };
    something(function (value){
        callback(value);
    });
}
複製程式碼

下面是呼叫程式碼。

// 事件1
function func1(){
    return new Promise(function(resolve){
        // do something
        setTimeout(function(){
            console.log('func1');
            resolve();
        },1000);
    });
}

func1().then(function(){
    console.log('func1 complete');
});
複製程式碼

上面對Promise的封裝算是最簡單的版本,只是模擬了Promise的呼叫方法,比如then,還有Promise的建構函式。但是這樣的封裝無法實現鏈式呼叫,鏈式呼叫的核心就是當呼叫某個方法的時候返回該物件本身或者該物件對應class的全新物件。而對於Promise的改造也很簡單.

  1. then方法返回Promise本身
  2. Promoise需要支援多個callback
function Promise(something){
    var callbacks = [];
    this.then = function(onCompelete){
        callbacks.push(onCompelete);
        return this;
    };
    function resolve(value){
        callbacks.forEach(function(cb){
            cb(value);
        });
    }
    something(resolve);  
}
複製程式碼

呼叫程式碼如下:

func1().then(function(){
    console.log('func1 complete');
}).then(function(){
    console.log('then2');
}).then(function(){
    console.log('then3');
});
複製程式碼

現在的Promise執行上面的程式碼後能夠得到正確的執行結果,但是有一個問題,如果我們想在then方法再呼叫一個返回promise的方法?比如這樣:

// 事件2
function func2(){
    return new Promise(function(resolve){
        // do something
        setTimeout(function(){
            console.log('func2');
            resolve();
        },1000);
    });
}

func1().then(func2).then(function(){
    console.log('all complete');
});
複製程式碼

輸出如下:

func1
all complete
func2
複製程式碼

你會發現雖然func2成功呼叫了,但是輸出順序亂了,我們期望的正確輸出順序應該是:

func1
func2
all complete
複製程式碼

分析下問題出在哪裡?問題就出在Promise中的callbacks,第一個then是在func1返回的Promise上呼叫的,而第二個then事實上還是在func1返回的Promise上呼叫的。然而我們希望的是,第二個then應該是在func2返回的Promise呼叫,這時候就需要考慮如何進一步改造Promise了。

對於then傳入的onCompelete函式引數,它是不知道這個函式具體是否會返回Promise,只有呼叫了onCompelete方法才能知道具體返回的資料。但是onCompelete是回撥函式,你無法直接在then中呼叫。因此需要考慮其他的方式。

如果then方法裡面返回一個新的Promise物件呢?用這個新的Promise作為中間代理,比如這樣:

function Promise(something){
    var callbacks = [];
    this.then = function(onCompelete){
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value){
        callbacks.forEach(function(cb){
            var ret = cb.onCompelete(value);
            cb.resolve(ret);
        })
    }
    something(resolve);  
}
複製程式碼

但是執行的時候你會發現輸出順序還是沒變,還是有問題的。那麼繼續分析問題出在哪裡? 通過除錯發現,resolve傳入的value有可能是promise物件,而我們已經在then方法裡面返回了新的promise物件了,交由該物件作為代理了。因此resolve傳入的value如果是promise物件的話,那麼就需要把當前promiseresolve處理權交出去,交給傳入的promise物件。相當於代理人把權力交還給實際應該處理的物件。可能有點繞,我再詳細的描述下

func1返回的promisep1then返回的promisep2resolve傳入的promise物件為p3,func2返回的promise物件為p4

上面一共提到4個promise物件。

說下描說下呼叫順序。

首先由func1建立p1,然後呼叫then方法建立了p2,然後再次呼叫了then方法,由p2建立了p3p2p3都是由then建立的代理人。

這時候func1中的非同步程式碼執行了,1秒過後由func1呼叫了p1resolve方法,並且將callbacks陣列內的方法依次呼叫,然後由cb.onCompelete(value)方法間接得到func2返回的p4,接著呼叫p2resolve方法將p4傳入。但是上面說了,p2只是個代理,應該把權力交還給p4來執行。這樣p4得到權力--回撥函式,當func2的非同步程式碼執行完畢後,由p4來執行回撥函式。

因此resolve方法需要進行如下改造。

function resolve(value) {
    // 交還權力,並且把resolve傳過去
    if (value && (typeof value.then === 'function')) {
        value.then.call(value, resolve);
        return;
    }
    callbacks.forEach(function (cb) {
        var ret = cb.onCompelete(value);
        cb.resolve(ret);
    });
}
複製程式碼

上面的程式碼就是交權的程式碼。這樣完全的Promise修改如下:

function Promise(something) {
    var callbacks = [];
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value) {
        if (value && (typeof value.then === 'function')) {
            value.then.call(value, resolve);
            return;
        }
        callbacks.forEach(function (cb) {
            var ret = cb.onCompelete(value);
            cb.resolve(ret);
        });
    }
    something(resolve);
}
複製程式碼

這樣修改過後,再執行如下程式碼:

func1().then(func2).then(function () {
    console.log('all complete');
});
複製程式碼

現在就能得到正確的執行結果了。

至此,一個簡單的Promise定義完了。這時候有一個問題,如果呼叫then方法之前resolve已經被執行了怎麼辦呢,豈不是永遠都得不到回撥了?比如這樣:

(new Promise(function (resolve) {
    resolve();
})).then(function(){
    console.log('complete');
});
複製程式碼

你會發現then裡面的回撥就不會執行了。其實這時候只需要做一個小小的改動就行了。改造如下:

function Promise(something) {
    var callbacks = [];
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value) {
        if (value && (typeof value.then === 'function')) {
            value.then.call(value, resolve);
            return;
        }
        setTimeout(function(){
            callbacks.forEach(function (cb) {
                var ret = cb.onCompelete(value);
                cb.resolve(ret);
            });
        },0);
    }
    something(resolve);
}
複製程式碼

你會發現,這裡只是在resolve方法裡面,將執行的回撥放入setTimeout中,並且timeout設為0。這裡稍微說下原理

在第一篇中提到setTimeout類似定時器,JS內容在執行setTimeout的回撥函式的時候使用執行緒排程的方式將回撥函式排程到JS執行緒執行。但凡涉及到執行緒排程那麼肯定需要等待JS執行緒空閒的時候才能排程過來。這時候將timeout設為0,相當於改變了程式碼執行順序。

在實際的開發過程中,上面的Promise程式碼還是缺少了一個功能,那就是狀態管理,比如:pendingfulfilledrejected。下面的程式碼繼續加入狀態管理的程式碼,先新增pendingfulfilled的狀態:

function Promise(something) {
    var callbacks = [];
    var state = 0;//0:pending,1:fulfilled
    var resultValue = null;
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            handleCallBack({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function handleCallBack(callback){
        switch(state){
            case 0:{
                callbacks.push(callback);
                break;
            }
            case 1:{
                var ret = callback.onCompelete(resultValue);
                callback.resolve(ret);
                break;
            }
            default:{
                break;
            }
        }
    }
    function resolve(value) {
        if (value && (typeof value.then === 'function')) {
            value.then.call(value, resolve);
            return;
        }
        state = 1;
        resultValue = value;
        setTimeout(function(){
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        },0);
    }
    something(resolve);
}
複製程式碼

下面再繼續加入reject功能。

function Promise(something) {
    var callbacks = [];
    var state = 0;//0:pending,1:fulfilled 2:reject
    var resultValue = null;
    this.then = function (onCompelete, onReject) {
        return new Promise(function (resolve) {
            handleCallBack({
                onCompelete: onCompelete,
                resolve: resolve,
                reject: onReject
            });
        });
    };
    function handleCallBack(callback) {
        switch (state) {
            case 0: {
                callbacks.push(callback);
                break;
            }
            case 1: {
                var ret = callback.onCompelete(resultValue);
                callback.resolve(ret);
                break;
            }
            case 2: {
                if(callback.reject){
                    var ret = callback.reject(resultValue);
                }
                callback.resolve(ret);
                break;
            }
            default: {
                break;
            }
        }
    }
    function reject(error) {
        state = 2;
        resultValue = error;
        setTimeout(function () {
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        }, 0);
    }
    function resolve(value) {
        if (value && (typeof value.then === 'function')) {
            value.then.call(value, resolve);
            return;
        }
        state = 1;
        resultValue = value;
        setTimeout(function () {
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        }, 0);
    }
    something(resolve,reject);
}
複製程式碼

OK,通過上面一步一步對Promise進行修改,基本上是把Promise的功能完善了。

從這個上面一步一步剖析Promise原理的過程中,我們發現,Promise本身並不提供非同步功能,Promise只是對函式的回撥功能進行了封裝,甚至可以理解為Promise就是一個回撥代理。但是正是有了這個回撥代理,使得我們的回撥方式發生了徹底的改變,甚至直接影響了專案的架構設計。而在平時的開發過程中,Promise在非同步程式設計中起到了幾乎不可替代的作用。

相關文章