從使用到原理,實現符合Promise A+規範的Promise方法

奮鬥的小小鳥發表於2018-07-30

傳統的非同步回撥程式設計最大的缺陷是:回撥地獄,由於業務邏輯非常複雜,程式碼序列請求好幾層;並行請求以前也要通過引用step、async庫實現。現在ES6推出了Promise,通過Promise的鏈式呼叫可以解決回撥地獄問題,通過Promise.all方法可以解決並行請求的問題。現在我們通過手寫Promise來徹底理解Promise的原理。

一、構造簡單Promise函式,實現Promise的then方法

先來看一下簡單的使用方法:

var promise=new Promise(function(resolved,rejected){
    console.log(1);
    resolved('123');
})
promise.then(function(data){
    console.log('success'+data)
},function(err){
    console.log('fail'+err)
})
複製程式碼

注意點:

  1. new Promise傳入的executor方法程式碼是同步執行的;
  2. promise物件的狀態有三種:pending(等待態),resolved(成功態),rejected(失敗態),只能從等待態轉化成成功態或失敗態;
  3. executor中執行resolve()方法,表示將轉化為成功態,promise.then呼叫時執行成功的方法,executor中執行reject()方法,表示將轉化為成功態,promise.then呼叫時執行失敗的方法。

思路:

  1. 首先構造一個類Promise,傳入一個引數executor方法;
  2. 用status記錄Promise狀態,預設是pending,成功態是resolved,失敗態是rejected, execuotor執行時,重寫resolve、reject方法,執行到這兩個方法時,將promise狀態修改,並將引數儲存起來,以便then方法呼叫; 3.當例項執行then方法時,依據promise狀態來執行成功方法或失敗方法。

這樣就實現了這個簡單的peomise,程式碼如下:

function Promise(executor){
    let self=this;
    self.status='pending';
    self.value=undefined;
    self.reason=undefined;
    function resolve(value){
        if(self.status==='pending'){
            self.value=value;
            self.status='resolved';
        }
    }
    function reject(reason){
        if(self.status==='pending'){
            self.reason=reason;
            self.status='rejected';
        }
    }
    try{
        executor(resolve,reject)  //第一步:先執行executor,根據裡面resolve和reject的呼叫,執行上面的resolve和reject方法
    }catch(e){
        reject(e);
    }
}
Promise.prototype.then=function(onFulfilled,onRejected){
    let self=this;//第二步:then被呼叫的時候根據已經記錄的promise狀態,執行成功方法或失敗方法
    if(self.status==='resolved'){
        onFulfilled(self.value);
    }
    if(self.status==='rejected'){
        onRejected(self.reason);
    }
}
module.exports = Promise
複製程式碼

二、實現Promise executor中的非同步呼叫

先來看一下簡單使用小例子:

var promise=new Promise(function(resolve,reject){
    console.log('hello')
    setTimeout(function(){
        resolve('ok')
    },1000);
})
promise.then(function(data){
    console.log('success'+data)
},function(err){
    console.log('fail'+err)
})
複製程式碼

上面例子,先列印出hello,過1秒後執行resolve方法,列印出successok。

注意點:

  1. executor執行一秒之後再呼叫resolve方法,才會執行then中的成功方法;

思路:

  1. executor中方法沒呼叫resolve之前,Promise方法一直是pending狀態,這時候執行器已經執行完了then方法,這時我們把then方法中的成功方法和失敗方法存入陣列,當resolve方法呼叫時,去執行這些儲存起來的方法;
  2. 沒有非同步呼叫的話,executor執行時執行resolve方法,成功陣列 為空,所以不會被影響,還是在then方法中去執行成功或失敗方法。
function Promise(executor){
    let self=this;
    self.status='pending';
    self.value=undefined;
    self.reason=undefined;
    self.onResolvedCallbacks=[];
    self.onRejectedCallbacks=[];
    function resolve(value){
        if(self.status==='pending'){
            self.value=value;
            self.status='resolved';
            self.onResolvedCallbacks.forEach(item=>item());//最終執行resolve方法時,修改狀態,將成功陣列中的方法取出,一一執行。
        }
    }
    function reject(reason){
        if(self.status==='pending'){
            self.reason=reason;
            self.status='rejected';
            self.onRejectedCallbacks.forEach(item=>item());//最終執行reject方法時,修改狀態,將失敗陣列中的方法取出,一一執行。
        }
    }
    try{
        executor(resolve,reject)
    }catch(e){
        reject(e);
    }
}
Promise.prototype.then=function(onFulfilled,onRejected){
    let self=this;
    if(self.status==='resolved'){
        onFulfilled(self.value);
    }
    if(self.status==='rejected'){
        onRejected(self.reason);
    }
    if(self.status==='pending'){ //執行then的時候是等待態,就將成功方法存入onResolvedCallbacks陣列,將失敗方法存入onRejectedCallbacks陣列
        self.onResolvedCallbacks.push(function(){
            onFulfilled(self.value);
        });
        self.onRejectedCallbacks.push(function(){
            onRejected(self.reason);
        });
    }
}
module.exports = Promise
複製程式碼

三、實現Promise then的鏈式呼叫

鏈式呼叫是Promise中的核心方法,也是最重要的一個方法,先來看一下鏈式呼叫的用法:

let p=new Promise((resolve,reject)=>{
    console.log(1);
    setTimeout(function(){
        resolve('123'); 
    },1000);
})
let p2=p.then((data)=>{
    console.log(data);
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            resolve('456');
        },1000);
    });
},err=>{
    console.log(err);
    throw Error('失敗');
}).then((data)=>{
    console.log('aaa'+data);
},(err)=>{
    console.log(err);
})
複製程式碼

上面程式碼首先列印出executor中的1,1秒之後執行resolve方法執行第一個then中的成功方法,先列印出123,再執行此方法中return的Promise,一秒之後執行下一個then的成功方法,列印出aaa456。

注意點:

  1. 如果then方法返回一個普通值,執行下一個then的成功方法;
  2. 如果丟擲一個錯誤,執行下一個then的失敗方法;
  3. then方法中返回一個Promise物件,等待Promise物件執行成功就呼叫下個then的成功方法,這個Promise呼叫reject()就執行下一個then的失敗方法;

思路:

1.在then方法後之所以可以接著呼叫then方法,肯定then方法需要返回一個Promise例項;

  1. 在then方法中我們用x去接收成功或失敗方法的返回值,當成功或失敗方法丟擲錯誤,直接執行reject,呼叫下一個then的失敗方法,專門寫了一個方法resolvePromise分析返回值;
  2. resolvePromise方法中傳入了四個引數,then中的Promise2,x值;
  3. 如果x與promise相等,報型別錯誤迴圈引用;
  4. 當x不是null,是個物件型別或者是個函式型別,並且他的then方法也是函式型別時,我們認為x是一個promise例項,執行x.then()方法;再用resolvePromise方法分析他的成功的返回值;
  5. 如果x不是null不是object或function,我們認為x是一個普通值,執行promise2中的resolve方法;
  6. 如果x.then方法獲取失敗,呼叫promise2的reject方法;
  7. 定義一個called為true,當執行過resolve或reject方法後將其變為false,保證resolve和reject方法只執行一次;
  8. 這裡面存在兩個遞迴呼叫,一個是then方法裡面return new Promise方法,一個是resolvePromise方法,第一個的遞迴呼叫解決了executor中非同步呼叫的問題,第二個遞迴呼叫解決了return Promise例項的問題。

程式碼如下:

function Promise(executor){
    let self=this;
    self.status='pending';
    self.value=undefined;
    self.reason=undefined;
    self.onResolvedCallbacks = []; // 存放then成功的回撥
    self.onRejectedCallbacks = []; // 存放then失敗的回撥
 
    function resolve(value){
        if(self.status==='pending'){  //這裡面的self不能用this替代,因為是回撥執行的時候不是在類裡面所以這裡的this不是類函式
            self.status='resolve';
            self.value=value;
            self.onResolvedCallbacks.forEach(item=>{item()})
        }
    }
    function reject(reason){
        if(self.status==='pending'){
            self.status='reject';
            self.reason=reason;
            self.onRejectedCallbacks.forEach(item=>{item()})
        }
    }
    try{
        executor(resolve,reject);
    }catch(e){
        reject(e);
    }
}
function resolvePromise(promise2,x,resolve,reject){
    if(promise2===x){//自己等於自己,報型別錯誤
         return reject(new TypeError('迴圈引用了'));
    }
    let called;//控制成功或失敗只能呼叫一次
    if(x!==null && (typeof x==='object'||typeof x==='function')){   
        try{
            let then=x.then;
            if(typeof then==='function'){
                then.call(x,function(y){  //y可能還是一個promise,遞迴解析,直到返回普通值
                    if(called)return;
                    called=true;
                    resolvePromise(promise2,y,resolve,reject)
                },function(err){
                    if(called)return;
                    called=true;
                    reject(err);
                });
            }else{
                resolve(x);
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e);
        }
    } else { // 說明是一個普通值
        resolve(x); 
    }

}
Promise.prototype.then=function(onFufilled,onReject){
    onFufilled=typeof onFufilled==='function'?onFufilled:function(value){
        return value;
    }
    onReject=typeof onReject==='function'?onReject:function(err){
        throw err;//丟擲錯誤,才會走下個then的失敗
    }
    let self=this;
    let promise2;
    if(self.status=='pending'){
        promise2=new Promise(function(resolve,reject){
            self.onResolvedCallbacks.push(function(){
                setTimeout(function(){
                    try{
                        let x=onFufilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    }catch(e){
                        reject(e)
                    }
                })
            });
            self.onRejectedCallbacks.push(function(){
                setTimeout(function(){
                    try{
                        let x=onReject(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    }catch(e){
                        reject(e);
                    }
                })
            });
        })
    }
    if(self.status=='resolve'){
        promise2=new Promise(function(resolve,reject){
            setTimeout(function(){
                try{
                    let x=onFufilled(self.value);
                    resolvePromise(promise2,x,resolve,reject);
                }catch(e){
                    reject(e)
                }
            })
        })
    }
    if(self.status=='reject'){
        promise2=new Promise(function(resolve,reject){
            setTimeout(function(){
                try{
                    let x=onReject(self.reason);//這裡的x可能是一個普通值,也可能是一個promise,所以寫個方法統一處理。
                    resolvePromise(promise2,x,resolve,reject);
                }catch(e){
                    reject(e);
                }
            })
        })

    }
    return promise2;
};
複製程式碼

四、檢測一下我們的Promise是否符合Promise A+規範

我們需要先給Promise類增加一個defer方法,其實就是一個語法糖。

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise(function (resolve, reject) {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd
}
複製程式碼

沒有上面的語法糖,無法被檢測。 下載一個外掛npm install -g promises-aplus-tests 在命令列工具中執行命令:npm install -g promises-aplus-tests ./promise.js 即可檢驗自己的promise寫的如何。

五、附贈Promise的all、race、resolve、reject、catch方法

Promise.all=function(promises){
    //promises是一個promise的陣列,同時需要返回一個promise
    return new Promise(function(resolve,reject){
        let arr=[];    //arr是最終返回值的結果集
        let i=0;
        function processData(index,y){
            arr[index]=y;
            if(++i===promises.length){
                resolve(arr);
            }
        }
        for(let i=0;i<promises.length;i++){  //因為then的時候,for迴圈已經走了,i值就是最大的,所以這裡用let,保證每次i都是都是當前值
            promises[i].then(function(y){
                processData(i,y);
            },function(err){
                reject();
            })
        }
    })
}
Promise.race=function(promises){
    return new Promise(function(resolve,reject){
        for(let i=0;i<promises.length;i++){
            promises[i].then(function(data){
                resolve(data);
            },function(err){
                reject(err);
            })
        }
    })

}
Promise.resolve=function(value){
    return new Promise(function(resolve,reject){
        resolve(value);
    })
}
Promise.reject=function(reason){
    return new Promise(function(resolve,reject){
        reject(reason);
    })
}
Promise.prototype.catch = function (callback) {
    return this.then(null, callback)
}
複製程式碼

相關文章