10分鐘搞定面試難點之Promise原理

Promise標準

1.Promise規範

Promise規範有不少,如Promise/A,Promise/B,Promise/D以及Promise/A的升級版Promise/A+。ES6 中採用了Promise/A+ 規範。 什麼是 Promise/A+ 規範,推薦一篇文章Promises/A+規範(中文)jquery

2.Promise標準解讀

1.一個promise的當前狀態只能是pendingfulfilledrejected三種之一。狀態改變只能是pendingfulfilled或者pendingrejected,而且狀態改變不可逆的。 2.promisethen方法接收兩個可選參數,表示該promise狀態改變時的回調(promise.then(onFulfilled, onRejected))。then方法返回一個promisethen方法能夠被同一個promise調用屢次。數組

Promise的使用

const promise = new Promise((resolve) => {
    setTimeout(()=> {
        resolve(1);
    }, 2000);
});
promise.then(a=> alert(a));
複製代碼

上面這段代碼的解讀:promise

  • 構造函數接收一個executor當即執行函數;
  • executor當即執行函數接收一個resolve函數;
  • promise對象的then方法綁定狀態變爲fulfilled時的回調,then方法也能夠被稱爲註冊函數(這句話怎麼理解呢?不着急,下面會解釋);
  • resolve函數被調用時會觸發then方法中註冊的回調函數(這句話很重要,在我看來,能理解這句話,就能理解promise);

用你們都會用,那麼,咱們來實現一個最最最簡單的promisebash

實現簡單的promise

直接看下面這段代碼異步

function MyPromise(executor){
    var self = this;
    this.status = "pending";
    this.data = "";
    this.resolveArr = [];
    this.rejectArr = [];
    function resolve(data){
        //問題:爲啥要在這裏作異步處理(如過如今不能理解,等看完必定能理解的)
        self.data = data
        setTimeout(function(){
            self.resolveArr.forEach((fn)=>{
                fn(data);
            });
        },0);
    }

    function reject(){}

    try{
        executor(resolve,reject)
    }catch(err){
        reject(err)
    }
}

MyPromise.prototype.then = function(resolveFn,rejectFn){
    this.resolveArr.push(resolveFn);
    this.rejectArr.push(rejectFn);
}
複製代碼

來解釋一下上邊這段代碼函數

  • MyPromise接收了一個executor做爲參數,在實例化的時候去執行;這個executor接收兩個參數resolvereject(註釋:和原生的Promise(function(resolve,reject){})是保持一致的);測試

  • 接下來看resolvereject這倆參數,在MyPromise函數體內,能夠看到,這兩個參數實際上是定義好的,能夠理解爲一個成功回調和一個失敗的回調,和原生的Promise保持一致(在接下來的講解中,基本以成功回調的resolve來作說明);來看下resolve函數內部作了什麼?發現resolve只作了一件事,遍歷了函數體內定義的resolveArr,而後執行了resolveArr了每一項,並把data參數傳遞進去;優化

那麼問題來了,既然resolveArr的每一項均可以執行,那就是函數咯,咱們能夠看到,在函數體內,咱們只定義了一個空的resolveArr,那這個函數是怎麼來的呢?ui

  • 在代碼的最後面發現了一個掛載在原型上的then方法裏面有用到這個數組(在文章的前面有說到,then方法能夠理解爲註冊方法,怎麼個註冊法呢?),原來在這個then方法會接收兩個函數類型的參數(和原生的Promise保持一致);並把第一個參數添加到resolveArr中,第二個參數添加到rejectArr中。這也就完成了註冊!

說了那麼多,來測試一下,是否有效:this

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("測試");
    },1000);
});

test.then((data)=>{
    console.log(data);
});
複製代碼

運行結果以下:

能夠看到1秒鐘之後會打印出結果(因爲技術緣由,不會作gif圖,見諒);

重點再次提醒:時刻記住一句話,resolve函數被調用時會觸發then方法中註冊的回調函數,從上面這個案例能夠充分的體現;

理解了上面這個案例之後,咱們來慢慢的作優化

優化一

上面提到,then方法返回一個promise。then 方法能夠被同一個 promise 調用屢次。來改寫下then方法:

MyPromise.prototype.then = function(onResolved,onRejected){
    this.resolveArr.push(onResolved);
    this.rejectArr.push(onRejected);
    return this;
}
複製代碼

只須要在then的最後return this便可,看過jquery源碼的同窗應該都很熟悉吧! 問題點:這樣作的話,全部then方法註冊的函數所接收的值都是同樣的,這顯然是咱們不能接收的,後面還會優化

優化二

上面提到,一個promise的當前狀態只能是pendingfulfilledrejected三種之一。狀態改變只能是pendingfulfilled或者pendingrejected。來改下函數體內的resolve方法:

function resolve(data){
  if(self.status==="pending"){
    self.status = "fulfilled";
    setTimeout(function(){
      self.resolveArr.forEach((fn)=>{
        fn(data);
      });
    },0)
  }
}
複製代碼

只需在遍歷resolveArr前判斷狀態是否爲pending,若是是,則改變狀態爲fulfilled,而後執行遍歷,這裏也解釋了文章前面提到的一句話(promise對象的then方法綁定狀態變爲fulfilled時的回調);

針對優化一提出的問題,來作進一步的優化

根據promise/A+規範優化以上代碼

文章開頭對promise/A+規範的標準解讀中提到(then方法返回一個promise),那咱們就給then方法返回一個promise,來改寫then方法:

MyPromise.prototype.then = function(onResolved,onRejected){
    var self = this;
    return new MyPromise(function(resolve,reject){
        self.resolveArr.push(function(){
            var x = onResolved(self.data);
            if(x instanceof MyPromise){  
                //當then返回的是一個MyPromise的時候,會把resove掛在到該MyPromise的resolveArr隊列中,等待該MyPromise執行對應的resolve;     
                x.then(resolve,reject)
            }else{
                //當then的返回值不是一個MyPromise的時候,直接執行resolve跳到下一個then處理;
                resolve(x);  
            }
        });
    });
}
複製代碼

再次強調:resolve函數被調用時會觸發then方法中註冊的回調函數

提問:這裏涉及了幾個MyPromise實例?
答案:2個或者3個;
爲何呢?
來看一下,代碼中有一句var x = onResolved(self.data),並在後面判斷了x是否是屬於MyPromise類型;從代碼中也能夠看出,x是函數onResolved的返回結果,那這個onResolved又是什麼呢?原來就是調用then方法時所註冊的函數;當x也是一個MyPromise實例的時候,這裏就涉及了三個MyPromise實例,分別是(當前實例(this),then方法返回的實例,註冊函數返回的實例(x));

分析

爲了方便,我把當前實例稱爲Athen方法返回的實例稱爲B; 當A調用then方法的時候,直接返回了B,並在B初始化的時候,給AresolveArr加入一個匿名函數(這裏記爲AResolveFn),當A中的resolve執行的時候,會去執行這個AResolveFn,在這個AResolveFn中,會去執行咱們傳入then中的onResolved方法,並把返回結果記x

狀況一:當x不爲MyPromise

從代碼中能夠看出,在這種狀況下,直接執行了resolve(x);這個resolveBresolve,那Bresolve執行了,就會觸發B實例用then方法註冊的方法,這樣就實現了then的鏈式調用,而且把每次註冊方法的返回值傳下去啦!
測試:

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("測試");
    },1000);
});

test.then((data)=>{
    console.log(data);
    return "測試2"
}).then((data)=>{
    console.log(data);
});
複製代碼

測試結果:

打印結果沒問題。

狀況二:當x爲MyPromise

從代碼上看,當xMyPromise的時候,直接把Bresolve當作了x的註冊方法;這樣的話,只有當xresolve執行的時候,會觸發xthen註冊的方法,纔會觸發Bresolve,纔會觸發Bthen註冊的方法;
流程就是:Aresolve --> Athen註冊的方法 --> xresolve --> xthen註冊的方法 --> Bresolve --> Bthen註冊的方法,這樣就實現了異步鏈式傳遞;

測試:

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("測試");
    },1000);
});

test.then((data)=>{
    console.log(data);
    return new MyPromise(function(resolve){
        setTimeout(()=>{
            resolve("測試2");
        },1000)
    })
}).then((data)=>{
    console.log(data);
});
複製代碼

測試結果:

因爲沒有作gif圖,看不出效果,可是從圖中的執行時間能夠看出來,整個過程確實是執行了2秒多;

針對狀態改變的優化

以上說到的都是正常的狀態下;就是說,當你用then方法註冊函數的時候,都是在pending狀態;可是,有的特殊場景,在你用then方法的時候,狀態已是fulfilled,就像下面這樣:

var test = new MyPromise(function(resolve,reject){
    setTimeout(function(){
        resolve("test")
    },1000);
});

setTimeout(function(){
    test.then(function(data){
        console.log(data);
    });
},2000)
複製代碼

在這種狀況下,當調用resolve方法的時候,並無用then方法註冊函數,那麼resolveArr必然是一個空數組;此時狀態是fulfilled,不可能說你用then方法註冊的時候,再給你執行一邊,也違背了Promise的標準。

優化then方法

MyPromise.prototype.then = function(onResolved,onRejected){
    var self = this;
    if(self.status==="fulfilled"){
        return new MyPromise(function(resolve,reject){
            var x = onResolved(self.data);
            if(x instanceof MyPromise){
                x.then(resolve,reject)
            }else{
                resolve(x);
            }
        });
    }

    if(self.status==="pending"){
        return new MyPromise(function(resolve,reject){
            self.resolveArr.push(function(){
                var x = onResolved(self.data);
                if(x instanceof MyPromise){
                    x.then(resolve,reject)
                }else{
                    resolve(x);
                }
            });
        });
    }
}
複製代碼

then方法中作了一層判斷,當狀態是fulfilled的時候,不會執行resolve,固然也不會觸發then註冊的函數,這裏的作法是跳過這一層,直接去執行onResolved,而後繼續走下去;上面原理都理解的話,這裏理解起來應該也不是特別困難,這裏就不作過多解釋了,實在理不清楚的話就留言給我吧!

原理到這裏基本上就差很少了,這裏再補充一個promise靜態方法

promise靜態方法

Promise中有許多靜態方法,像Promise.all,Promise.race,那咱們用以上本身實現的MyPromise來加一個靜態方法試試:

MyPromise.all = function(promiseArr){
    return new MyPromise(function(resolve){
        var length = promiseArr.length;
        var resultIndex = 0;
        var resultArr = [];
        promiseArr.forEach((promise)=>{
            promise.then(function(data){
                resultArr.push(data);
                resultIndex++;
                if(resultIndex===length){
                    resolve(resultArr);
                }
            });
        });
    });
}
複製代碼

原理理解的話,這個實現起來也就不難了,這裏就不講解了,直接來測試一下吧:

var test1 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test1")
    },1000)
});
var test2 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test2")
    },1500)
});
var test3 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test3")
    },1200)
});
複製代碼

結果以下:

結果沒有問題吧,返回了一個數組,很開心有不有!有興趣的小夥伴能夠跟着這個思路,去實現一下其餘靜態方法。

總結

關於promise的內容到這裏就差很少了,平時也比較少發表文章,總怕本身總結的很差,但願能給你們帶來幫助吧!

相關文章
相關標籤/搜索