1. 程式人生 > >拆開 JavaScript Promise 理解一波再組裝起來

拆開 JavaScript Promise 理解一波再組裝起來

準備好材料、工具,開拆!!!

一個 Promise 的運用:

var firstPromise = new Promise(function(resolve,reject){
    setTimeout(function(){
        var result = Math.random() <= 0.5 ? 1:0;
        if(result){
            resolve('resolved');
        }else{
            reject('rejected')
        }
    },1000)
})

var secondPromise = new
Promise(function(resolve,reject){ setTimeout(function(){ var result = Math.random() <= 0.5 ? 1:0; if(result){ resolve('resolved'); }else{ reject('rejected') } },2000) }) firstPromise.then(function(value){ console.log(value); return
secondPromise; },function(reason){ console.log(reason); return secondPromise; }).then(function(value){ console.log(value); },function(reason){ console.log(reason); }) // 1s後隨機輸出結果 resolved 或者 rejected // 再1s後隨機輸出結果 resolved 或者 rejected

效果如上,在一個 promise 被完成/被拒絕時執行對應的回撥取到非同步結果。

同時,以上程式碼使用 promise 避免了回撥地獄,規範了回撥操作。

接下來,把 promise 拆成幾塊,學習一下怎麼樣的實現過程。

板塊一、Promise 建構函式

建立 promise 物件的建構函式,是創造 promise 的工廠。

基礎要求:Promise 函式僅產生一個物件,避免大量變數的汙染,將該藏好的物件/值藏好,該暴露的暴露;Promise 接收一個函式作為引數,且該函式在構造 promise 物件時被執行;Promise 必須有個 .then 方法(後續方法可自行擴充套件)。

function Promise(fn){
    this.then = function(){ };
}

板塊二、初始化過程,處理引數fn

Promise 建構函式引數 fn 中傳入 resolve/reject;Promise 初始化的時候執行 fn 並在 promise 得到最終結果後執行傳入的 resolve/reject ;resolve/reject 函式中執行 promise 中指定的完成/拒絕時回撥函式,並以最終結果作為引數。

function Promise(fn){

    // 完成時
    function resolve(value) {
        console.log('value ',value);
    }

    // 拒絕時
    function reject(reason) {
        console.log('reason ',reason);
    }

    // 執行傳入的fn
    function init(fn, onResolved, onRejected) {
        try {
            fn(function (value) {
                onResolved(value);
            }, function (reason) {
                onRejected(reason);
            })
        } catch (err) {
            onRejected(err);
        }
    }

    init(fn, resolve, reject);
    
    this.then = function(){ };
}

var promise = new Promise(function(resolve,reject){ 
    setTimeout(function(){
        var result = Math.random() <= 0.5 ? 1:0;
        if(result){
            resolve('resolved') 
        }else{
            reject('rejected')
        }
    },1000) 
})

// 1s後隨機輸出 value resolved 或者 reason rejected

板塊三、.then 裡的處理流程

在promise中, .then 將傳入的 resolvedHandle 和 rejectedHandle 函式存入 promise 的 handlers 中作為回撥列表中的一項,在需要的時候(Promise被完成的時候)攜帶最終結果執行。

首先,假設有個非同步操作,而且已經知道回撥函式是什麼,程式碼如下:

var resolvedHandle = function(res){ console.log(res) };
var rejectedHandle = function(err){ console.log(err) };

setTimeout(function(){
    var result = Math.random() <= 0.5 ? 1:0;
    if(result){
        resolvedHandle('resolved');
    }else{
        rejectedHandle('rejected');
    }
},1000)

// 1s後輸出 resolved 或者 rejected

而對於 promise 而言,回撥函式是在 .then 中傳入並且在 promise 中給定義的,並且為了實現鏈式的操作, .then 中必須有返回一個物件,且物件須是一個攜帶 .then 方法的物件或函式或為一個 promise ,才足以繼續執行.then。

// fn 作為初始化Promise時傳入的函式,應該被立即執行並取出其中的呼叫
function Promise(fn) {

    var $resolveHandle = function (res) { };
    var $rejectHandle = function (err) { };

    // 執行Promise被完成時函式
    function resolve(value) {
        try {
            var then = getThen(value);
            if (then) {
                init(then.bind(value), resolve, reject);
                return;
            };
            fulfill(value);
        } catch (err) {
            reject(err);
        }
    }

    // 完成時
    function fulfill(value) {
        $resolveHandle(value);
        $resolveHandle = null;
    }

    // 拒絕時
    function reject(reason) {
        $rejectHandle(reason);
        $rejectHandle = null;
    }

    // 執行傳入的fn並執行回撥
    function init(fn, onResolved, onRejected) {
        try {
            fn(function (value) {
                onResolved(value);
            }, function (reason) {
                onRejected(reason);
            })
        } catch (err) {
            onRejected(err);
        }
    }

    init(fn, resolve, reject);

    function getThen(value) {
        var t = typeof value;
        if (value && (t === 'object' || t === 'function')) {
            var then = value.then;
            if (typeof then === 'function') {
                return then;
            }
        }
        return null;
    };

    this.then = function (resolveHandle, rejectHandle) {
        return new Promise(function (resolve, reject) {
            $resolveHandle = function (result) {
                resolve(resolveHandle(result));
            }
            $rejectHandle = function (reason) {
                resolve(rejectHandle(reason));
            }
        })
    }
}

var firstPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        var result = Math.random() <= 0.5 ? 1 : 0;
        if (result) {
            resolve('resolved');
        } else {
            reject('rejected');
        }
    }, 1000);
})

var secondPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        var result = Math.random() <= 0.5 ? 1 : 0;
        if (result) {
            resolve('resolved 2');
        } else {
            reject('rejected 2');
        }
    }, 2000);
})

firstPromise.then(function (res) {
    console.log('res ', res);
    return secondPromise;
}, function (err) {
    console.log('rej ', err);
    return secondPromise;
}).then(function (res) {
    console.log('res 2 ', res);
}, function (err) {
    console.log('rej 2 ', err);
})

// 1s後隨機輸出 res resolved  或者  rej rejected
// 又1s後輸出 res 2 resolved 2 或者 rej 2 rejected 2 

至此,上面的程式碼基本算是滿足了一個 promise 的實現思路,但離正規軍 promise 實現還存在一段距離

o(╥﹏╥)o...接下去學習吧。

板塊四、Promise/A+規範

由於 Promise/A+規範較長,就不放到文章裡了,給連結吧(中午版是自己翻譯的,有出入的地方還請以英文原版為準)

Promise/A+ 規範 [ 原文 ]

Promise/A+ 規範 [ 譯文 ]

對照promise/A+規範,以上的Promise程式碼還存在問題:

  1.promise還需要儲存promise狀態和最終結果,以便後續被多次使用;

  2.同一個promise的.then方法中註冊的回撥函式可被多次執行,且回撥函式可以是個列表;

  3.事件排程,回撥函式應該在本輪.then方法所在事件佇列結束後被呼叫;

  4.捕捉錯誤並做拒絕處理;

  更多細節...

繼續改進,最後整改後的程式碼大致是這樣的:

function Promise(fn) {
    /* state
    * 0 : pending 
    * 1 : resloved 
    * 2 : rejected 
    */
    var state = 0;
    var value = null;
    var handlers = [];

    function fulfill(result) {
        state = 1;
        value = result;
        handlers.forEach(handle);
        handlers = [];
    };

    function reject(error) {
        state = 2;
        value = error;
        handlers.forEach(handle);
        handlers = [];
    };

    function resolve(result) {
        try {
            var then = getThen(result);
            if (then) {
                init(then.bind(result), resolve, reject);
                return;
            }
            fulfill(result);
        } catch (err) {
            reject(err);
        }
    };

    function getThen(value) {
        var type = typeof value;
        if (value && (type === 'object' || type === 'function')) {
            var then = value.then;
            if (typeof then === 'function') {
                return then;
            }
        }
        return null;
    };

    function handle(handler) {
        if (state === 0) {
            handlers.push(handler);
        } else {
            if (typeof handler.onResolved === 'function') {
                if (state === 1) {
                    handler.onResolved(value);
                };
                if (state === 2) {
                    handler.onRejected(value);
                };
            }
        }
    };

    // 放到事件佇列最後,在本輪事件執行完後執行
    function timeHandle(callback, newValue) {
        setTimeout(function () {
            callback(newValue);
        }, 0)
    }

    function init(fn, onResolved, onRejected) {
        try {
            fn(function (value) {
                timeHandle(onResolved, value);
            }, function (reason) {
                timeHandle(onRejected, reason);
            });
        } catch (err) {
            timeHandle(onRejected, err);
        }
    };

    init(fn, resolve, reject);

    this.then = function (onResolved, onRejected) {
        if (!onResolved && !onRejected) {
            throw new TypeError('One of onResolved or onRejected must be a function.')
        };
        return new Promise(function (resolve, reject) {
            handle({
                onResolved: function (result) {
                    if (typeof onResolved === 'function') {
                        try {
                            resolve(onResolved(result));
                        } catch (err) {
                            reject(err);
                        }
                    } else {
                        resolve(result);
                    }
                },
                onRejected: function (error) {
                    if (typeof onRejected === 'function') {
                        try {
                            resolve(onRejected(error));
                        } catch (err) {
                            reject(err);
                        }
                    } else {
                        reject(error);
                    }
                }
            })
        })
    };
}

var firstPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        var result = Math.random() <= 0.5 ? 1 : 0;
        if (result) {
            resolve('resolved 1');
        } else {
            reject('rejected 1');
        }
    }, 1000);
})

var secondPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        var result = Math.random() <= 0.5 ? 1 : 0;
        if (result) {
            resolve('resolved 2');
        } else {
            reject('rejected 2');
        }
    }, 3000);
})

firstPromise.then(function (res) {
    console.log('res 1 ', res);
    return secondPromise;
}, function (err) {
    console.log('rej 1 ', err);
    return secondPromise;
}).then(function (res) {
    console.log('res 2 ', res);
}, function (err) {
    console.log('rej 2 ', err);
})

/* *
 * 1s後輸出 res 1 resolved 1 或者 rej 1 rejected 1
 * 2s後輸出 res 2 resolved 2 或者 rej 2 rejected 2
 * */

通過板塊一、二、三的知識點,即可大致摸清promise的實現;板塊四加上一些補充和限制,遵循一些規範,提高promise功能的可擴充套件性。

學會了怎麼理解promise,更重要的是學會正確的使用它。

正確使用 Promise

promise 在業務開發中多用來處理非同步或者多層回撥的情況。

基礎使用 Promise MDN 及相關介紹文件中的案例為準,這裡不一一贅述了... 這裡簡單的列出兩個在使用 promise 過程中比較需要注意的點:

1. 不同平臺環境下 Promise 的方法和遵循規則略微有些出入,詳情以各平臺環境下的 Promise 物件為基準。

  如 es6 Promise 存在Promise.race,Promise.all等方法,node中則沒有這些方法。

  如 瀏覽器 Promise 事件排程走的是 setTimeout,node 走的是 process.nextTick 。(參考 [asap] )

2. Promise 雖可解決回撥操作的規範問題(回撥地獄),但也不能濫用 Promise (可能會佔用過多記憶體)。

promise 解決後的結果會被存於記憶體中,被對應 promise 引用著,將上面的最終程式碼中測試的兩個 promise 改成如下:

var firstPromise = new Promise(function (resolve, reject) {
        setTimeout(function () {
            var result = Math.random() <= 0.5 ? 1 : 0;
            var str = '';
            var i = 0, num = 500000;
            for (; i < num; i++) {
                str += 'promise '
            }
            if (result) {
                resolve('resolved 1 : ' + str);
            } else {
                reject('rejected 1 : ' + str);
            }
        }, 1000);
    })

則記憶體佔用情況如下:

這些是一些平臺差異或業務需求方面的不同點,對 Promise 核心實現並影響甚微,對 Promise 擴充套件方法有影響,對業務中 Promise 的使用有影響。

參考

1. Promise/implementing 

2. Promise 實現程式碼閱讀