手動實現一個滿足promises-aplus-tests的Promise

citystrawman發表於2018-03-16

Promise 簡介

(該部分轉載自 blog.csdn.net/u010576399/… ) Promise物件是CommonJS工作組提出的一種規範,目的是為非同步操作提供統一介面.

那麼,什麼是Promises?

首先,它是一個物件,也就是說與其他JavaScript物件的用法,沒有什麼兩樣;其次,它起到代理作用(proxy),充當非同步操作與回撥函式之間的中介。它使得非同步操作具備同步操作的介面,使得程式具備正常的同步執行的流程,回撥函式不必再一層層巢狀。

簡單說,它的思想是,每一個非同步任務立刻返回一個Promise物件,由於是立刻返回,所以可以採用同步操作的流程。這個Promises物件有一個then方法,允許指定回撥函式,在非同步任務完成後呼叫。

比如,非同步操作f1返回一個Promise物件,它的回撥函式f2寫法如下:

(new Promise(f1)).then(f2);
複製程式碼

這種寫法對於多層巢狀的回撥函式尤其方便。

// 傳統寫法
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // ...
      });
    });
  });
});

// Promises的寫法
(new Promise(step1))
  .then(step2)
  .then(step3)
  .then(step4);
複製程式碼

從上面程式碼可以看到,採用Promises介面以後,程式流程變得非常清楚,十分易讀。

注意,為了便於理解,上面程式碼的Promise物件的生成格式,做了簡化。

總的來說,傳統的回撥函式寫法使得程式碼混成一團,變得橫向發展而不是向下發展。Promises規範就是為了解決這個問題而提出的,目標是使用正常的程式流程(同步),來處理非同步操作。它先返回一個Promise物件,後面的操作以同步的方式,寄存在這個物件上面。等到非同步操作有了結果,再執行前期寄放在它上面的其他操作。

Promises原本只是社群提出的一個構想,一些外部函式庫率先實現了這個功能。ECMAScript 6將其寫入語言標準,因此目前JavaScript語言原生支援Promise物件。

手動實現一個滿足promises-aplus-tests的Promise

promisesaplus.com/ 是一個介紹promise如何實現的一個網站. 根據該網站提供的介紹資訊, 我們可以嘗試自己寫一個promise並使用提供的promises-aplus-tests工具對所寫的promise進行測試.

Step1

首先 實現promise最基本的功能: 即在promise建立以後執行執行器中的程式碼,在then的時候相應的函式得以執行. 需要注意的是,當執行器中有錯誤丟擲的時候,應該捕獲錯誤並直接執行reject

//實現功能: 當執行器中呼叫resolve(),則then中只執行onFulfiled方法,執行器中呼叫reject(),則then中只執行onRejected方法, 當兩個方法都有的時候,以先執行的方法為準,後執行的方法對then不產生影響.
function Promise (executor) {   //執行器 
    let self = this;
    self.status = 'pending';   //引入狀態,對兩個方法都有的情況進行區分
    self.value = undefined;    //預設值
    self.reason = undefined;
    function resolve(data_value) {
        if(self.status === 'pending') {
            self.status = 'resolved';
            self.value = data_value;
        }
    }
    function reject(data_reason) {
        if(self.status === 'pending') {
            self.status = 'rejected';
            self.reason = data_reason;
        }
    }
    //如果executor是同步程式碼 進行try catch獲取其中的異常 如果有異常 把異常傳到reject
    try {
        executor(resolve, reject);   
    } catch (e) {  
        reject(e);                 //呼叫reject並把捕獲的error作為引數傳給reject
    }
}

Promise.prototype.then = function (onFulfiled, onRejected) {
    let self = this;
    if(self.status === 'resolved') {
        onFulfiled(self.value);
    }
    if(self.status === 'rejected') {
        onRejected(self.value);
    }
}
複製程式碼

step2

那麼問題來了, 如果執行器中有非同步程式碼, 上面的實現方法就會出問題,因為在執行

executor(resolve, reject);   
複製程式碼

的時候,非同步程式碼不會立即執行, then中沒有判斷非同步程式碼是否已經執行的機制. 我們的解決方案是: 在then中判定狀態是否為pending,如果狀態為pending(此時為建構函式new Promise()執行期間), 則把then中的執行函式onFulfiled, onRejected先存入佇列(用array實現),當狀態改變後執行這些方法.

function Promise (executor) {   //執行器 
    let self = this;
    self.status = 'pending';
    self.value = undefined;    //預設值
    self.reason = undefined;
    self.onResolvedCallbacks = [];   //存放then成功的回撥 陣列
    self.onRejectedCallbacks = [];   //存放then失敗的回撥 陣列
    function resolve(data_value) {
        if(self.status === 'pending') {
            self.status = 'resolved';
            self.value = data_value;
            self.onResolvedCallbacks.forEach(function(fn) {   //呼叫resolve的時候執行儲存在onRejectedCallbacks的函式
                fn();
            })
        }
    }
    function reject(data_reason) {
        if(self.status === 'pending') {
            self.status = 'rejected';
            self.reason = data_reason;
            self.onRejectedCallbacks.forEach(function(fn) {   //呼叫resolve的時候執行儲存在onRejectedCallbacks的函式
                fn();
            })
        }
    }
    
    try {
        executor(resolve, reject);   //當executor中有非同步程式碼時 這部分不會立即執行(但是前面的部分在new的時候還是會執行)
    } catch (e) {  
        reject(e);                 
    }
}

Promise.prototype.then = function (onFulfiled, onRejected) {
    let self = this;
    if(self.status === 'resolved') {
        onFulfiled(self.value);
    }
    if(self.status === 'rejected') {
        onRejected(self.reason);
    }
    //當呼叫then時可能沒成功 也沒失敗
    if(self.status === 'pending') {             //此時沒有resolve也沒有reject
        self.onResolvedCallbacks.push(function(){       //用陣列是為了保證在非同步時有多次promise.then的情況 
            onFulfiled(self.value);
        });
        self.onRejectedCallbacks.push(function(){
            onRejected(self.reason);
        });
    }
}
複製程式碼

step3

到現在為止,我們僅僅完成了promise最基本的功能, 但是promise中一個重要的功能: then的鏈式呼叫尚未實現. 解決的思路類似於jquery的鏈式呼叫, 區別則是:jquery是返回this 這裡則是返回一個新的promise. 我們在then中新建一個變數promise2. 以

self.status === 'resolved'
複製程式碼

情況為例 將之前的程式碼修改為:

if(self.status === 'resolved') {
        promise2 = new Promise(function(resolve, reject){  //將前一次then中的執行函式放入新的Promise的executor得到promise2作為下次then的返回值
            onFulfiled(self.value);           //注意這時返回的promise的執行器執行的是onFulfiled函式 而不是resolve或者reject. 
        }) 
    }
複製程式碼

但是以上程式碼中, promise2在建立的時候, 並沒有設定resolve/reject的規則,因此只能算作半成品. 根據規定, 如果onFulfile/onRejected有返回值, 則將返回值作為resolve/reject的引數傳入,這樣,下一次.then就有狀態,不再是無根之木. 根據返回值的不同, 又將返回值分別以普通值和promise進行分別處理. 為此我們引入了一個統一的處理方法,以resolve()為例, 對應的處理方法我們取名為resolvePromise(promise2, x, resolve, reject) 其中x為onFulfiled的返回值. resolvePromise程式碼如下所示:

function resolvePromise(promise2, x, resolve, reject) {
    //有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫 
    if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
        return reject(new TypeError('迴圈引用'));
    }
    //看x是不是一個promise promise應該是一個物件
    if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
        //可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, function(y) {
                    //成功
                    resolvePromise(promise2, y, resolve, reject)
                }, function (err) {
                    //失敗
                    reject(err);
                })
            } else {
                resolve(x)             //如果then不是函式 則把x作為返回值.
            }
        } catch (e) {
            reject(e)
        }
        
    } else {  //普通值
        return resolve(x)
    }
}
複製程式碼

相應的

self.status === 'resolved'
複製程式碼

時的程式碼則為:

if(self.status === 'resolved') {
        promise2 = new Promise(function(resolve, reject){  
            let x = onFulfiled(self.value);    
            resolvePromise(promise2, x, resolve, reject);
        }) 
    }
複製程式碼

為例保證程式碼的通用性,考慮到有可能其他人的promise寫的不正確,可能會既呼叫成功又呼叫失敗的情況, 我們應當在程式碼中對返回的promise進行判斷: 如果兩個都呼叫 先呼叫誰 另一個忽略掉. 為此引入一個變數called.

function resolvePromise(promise2, x, resolve, reject) {
    //有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫 
    if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
        return reject(new TypeError('迴圈引用'));
    }
    //看x是不是一個promise promise應該是一個物件
    let called;  //表示是否呼叫過成功或者失敗
    if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
        //可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
        try {
            let then = x.then;
            if (typeof then === 'function') {
                //成功
                then.call(x, function(y) {
                    if (called) return
                    called = true;
                    resolvePromise(promise2, y, resolve, reject)
                }, function(err) {
                    if (called) return
                    called = true;
                    reject(err);
                })
            } else {
                resolve(x)             //如果then不是函式 則把x作為返回值.
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e)
        }
        
    } else {  //普通值
        return resolve(x)
    }
}
複製程式碼

Step4

到目前為止, promise的大框架基本完成. 接下來需要解決的兩個小問題是值的穿透以及onFulfiled/onRejected的非同步執行問題. 值的穿透的含義就是, 當then中使用沒有任何方法, onFulfiled()中的data自動作為返回值. 實現起來也很簡單, 在then的定義的最開始部分做一個判斷:

Promise.prototype.then = function (onFulfiled, onRejected) {
    //成功和失敗預設不傳, 給一個預設函式 可以實現值的穿透
    onFulfiled = typeof onFulfiled === 'function'? onFulfiled:function(value) {
        return value;
    }
    onRejected = typeof onRejected === 'function'? onRejected:function(err) {
        throw err;           //在值的穿透的情況下 應該走下一個then的onRejected而不是onFulfiled 保證邏輯的一致性
    }
    ..... 
}
複製程式碼

對於onFulfild/onRejected非同步執行的問題, 則是在

if(self.status === 'resolved') {
    promise2 = new Promise(function(resolve, reject){
    let x = onFulfiled(self.value);                             
    resolvePromise(promise2, x, resolve, reject) 
    ...
    }
}
複製程式碼

程式碼塊使用setTimeout. 這時帶來的副作用就是之前在promise建構函式中的程式碼塊

try {
        executor(resolve, reject);   
    } catch (e) {  
        reject(e);                 
    }
複製程式碼

不能捕獲到setTimeout中的非同步函式, 因此需要在setTimeout中也需要包一層try/catch:

if(self.status === 'resolved') {
        promise2 = new Promise(function(resolve, reject){  
            setTimeout(function(){                          //用setTimeOut實現非同步
                try {
                    let x = onFulfiled(self.value);        //x可能是普通值 也可能是一個promise, 還可能是別人的promise                               
                    resolvePromise(promise2, x, resolve, reject)  //寫一個方法統一處理 
                } catch (e) {
                    reject(e);                                        
                }
                
            }) 
        }) 
    }
複製程式碼

step5

最終程式碼如下所示:

function Promise (executor) {   
    let self = this;
    self.status = 'pending';
    self.value = undefined;    
    self.reason = undefined;
    self.onResolvedCallbacks = [];   
    self.onRejectedCallbacks = [];   
    function resolve(data_value) {
        if(self.status === 'pending') {
            self.status = 'resolved';
            self.value = data_value;
            self.onResolvedCallbacks.forEach(function(fn) {  
                fn();
            })
        }
    }
    function reject(data_reason) {
        if(self.status === 'pending') {
            self.status = 'rejected';
            self.reason = data_reason;
            self.onRejectedCallbacks.forEach(function(fn) {  
                fn();
            })
        }
    }
    try {
        executor(resolve, reject);   
    } catch (e) {  
        reject(e);                 
    }
}

function resolvePromise(promise2, x, resolve, reject) {
    //有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫 
    if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
        return reject(new TypeError('迴圈引用'));
    }
    //看x是不是一個promise promise應該是一個物件
    let called;  //表示是否呼叫過成功或者失敗
    if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
        //可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
        try {
            let then = x.then;
            if (typeof then === 'function') {
                //成功
                then.call(x, function(y) {
                    if (called) return        //避免別人寫的promise中既走resolve又走reject的情況
                    called = true;
                    resolvePromise(promise2, y, resolve, reject)
                }, function(err) {
                    if (called) return
                    called = true;
                    reject(err);
                })
            } else {
                resolve(x)             //如果then不是函式 則把x作為返回值.
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e)
        }
        
    } else {  //普通值
        return resolve(x)
    }

}

Promise.prototype.then = function (onFulfiled, onRejected) {
    //成功和失敗預設不傳給一個函式
    onFulfiled = typeof onFulfiled === 'function'? onFulfiled:function(value) {
        return value;
    }
    onRejected = typeof onRejected === 'function'? onRejected:function(err) {
        throw err;
    }
    let self = this;
    let promise2;  //新增: 返回的promise
    if(self.status === 'resolved') {
        promise2 = new Promise(function(resolve, reject){  
            setTimeout(function(){                          //用setTimeOut實現非同步
                try {
                    let x = onFulfiled(self.value);        //x可能是普通值 也可能是一個promise, 還可能是別人的promise                               
                    resolvePromise(promise2, x, resolve, reject)  //寫一個方法統一處理 
                } catch (e) {
                    reject(e);                                        
                }
                
            }) 
        }) 
    }
    if(self.status === 'rejected') {
        promise2 = new Promise(function(resolve, reject){
            setTimeout (function() {
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e);
                }
            })
        }) 
    }
    
    if(self.status === 'pending') {            
        promise2 = new Promise (function(resolve, reject) {   
            self.onResolvedCallbacks.push(function(){   
                setTimeout(function(){
                    try {
                        let x = onFulfiled(self.value); 
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }   
                })
            });
            self.onRejectedCallbacks.push(function(){
                setTimeout(function(){
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    } 
                })   
            });
        })
    }
    return promise2;
}
複製程式碼

使用promises-aplus-tests對該方法進行測試, 最終測試得到通過. 如圖所示:

Alt text
test result

當然, 該promise相對於原生的promise還有一些不同,比如沒有實現catch功能,沒有靜態方法等. 這些部分我們將下次進行詳細討論.

相關文章