Promise原理解讀

toymm發表於2018-11-29

promise是幹什麼的

在JavaScript的世界中,所有程式碼都是單執行緒執行的。由於這個“缺陷”,導致JavaScript的所有網路操作,瀏覽器事件,都必須是非同步執行。非同步執行可以用回撥函式實現,然而在需要多次回撥巢狀的時候,就容易進入回撥地獄了,promise解決了這一問題

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案–回撥函式和事件--更合理和更強大。它由社群最早提出和實現,ES6將其寫進了語言標準,統一了語法,原生提供了Promise。所謂Promise ,簡單說就是一個容器,裡面儲存著某個未來才回結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise是一個物件,從它可以獲取非同步操作的訊息。 Promise物件的狀態不受外界影響

promise是什麼樣的

  • promise物件上的方法
Promise.all()
Promise.race()
Promise.reject()
Promise.resolve()
Promise.prototype.catch()
Promise.prototype.finally()
Promise.prototype.then()
複製程式碼
  • 三種狀態:
    promise只有三種狀態
pending     進行中
fulfilled   已經成功
rejected    已經失敗
複製程式碼
  • 狀態改變:

Promise物件的狀態改變,只有兩種可能:

pending => fulfilled
pending => rejected
複製程式碼

基本用法

ES6規定,Promise物件是一個建構函式,用來生成Promise例項

const p = new Promise(function(resolve,reject){
    if(/*非同步操作成功*/){
        resolve(value);
    }else{
        reject(error);
    }
})
複製程式碼

resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;
reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

Promise 例項生成以後,可以用then 方法分別指定resolved狀態和rejected狀態的回撥函式。

p.then(function(value){
        //success
    },function(error){
        //failure
});
複製程式碼

原理解析

針對以上的基本用法,我們深入解析一下Promise的實現原理:

首先promsie有三種狀態,它是個建構函式,且接收一個函式作為引數,這個函式我們稱之為excutor,函式裡有兩個引數resolve和reject,這兩個回掉的作用我們在基本用法裡已經說了

跟jQuery鏈式呼叫相似,promise的鏈式呼叫是在 Promise.prototype.then()Promise.prototype.catch()裡返回了promise例項
這張圖清楚的描述了promise鏈式呼叫的過程

Promise原理解讀
  • 建構函式
const PENDING = `pengding`; // 初始態
const FULFILLED = `fulfilled`;// 成功態
const REJECTED = `rejected`;// 成功態

function Promise(excutor) {
    let self = this;
    self.status = PENDING; // 設定狀態
    // 定義存放成功回撥的陣列
    self.onResolveCallbacks = [];
    // 定義存放失敗回撥的陣列
    self.onRejectCallbacks = [];
    // 當呼叫此方法的時候,如果promise狀態為pending時可以以轉成成功態,如果已經是成功或者失敗則什麼都不做
    function resolve(value) {
        if (self.status == PENDING){ // 如果是初始態則轉成成功態
            self.status = FULFILLED;
            self.value = value; // 成功後會得到一個值且不能改變
            self.onResolveCallbacks.forEach(cb => cb(self.value))  // 呼叫所有成功的回撥
        }
    }
    function reject(reason) {
        if(self.status == PENDING){  // 如果是初始態則轉成失敗態
            self.status = REJECTED;
            self.value = reason;
            self.onRejectCallbacks.forEach(cb=>cb(self.value))
        }
    }
    try{
        // excutor函式執行可能會異常,需要捕獲,如果出錯需要用這個錯誤物件reject
        excutor(resolve,reject)
    }catch (e) {
        // excutor執行失敗需要用失敗原因reject這個promise
        reject(e)
    }
}
複製程式碼
  • then方法
    在基本用法中我們知道在呼叫then方法時傳進去的是兩個函式,分別對應的是promise中非同步方法成功和失敗時的回撥
Promise.prototype.then = function (onFulfilled, onRejected) {
    // 如果成功和失敗的回撥沒傳,表示這個then沒任何邏輯,只會把值往後拋
    onFulfilled = typeof onFulfilled == `function` ? onFulfilled : value=>value;
    onRejected = typeof onRejected == `function` ? onRejected : reason=>{ throw reason};
    let self = this;
    let promise2;
    // 為了實現鏈式呼叫,每一種狀態都要返回的是一個promise例項
    if(self.status == FULFILLED){ //如果promise狀態已經是成功態了,onFulfilled直接取值
        return promise2 = new Promise(function (resolve,reject) {
            setTimeout(function () { // 保證返回的promise是非同步
                try{
                    onFulfilled(self.value);
                }catch (e) {
                    // 如果執行成功的回撥過程中出錯,用錯誤原因把promise2 reject
                    reject(e)
                }
            })
        });
    }
    if(self.status == REJECTED){
        return promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try{
                    onRejected(self.value);
                }catch(e){
                    reject(e)
                }
            })
        })
    }
    if(self.status == PENDING){
        return promise2 = new Promise(function (resolve, reject){
            // pending狀態時把所有的回撥函式都新增到例項的兩個堆疊中暫存,等狀態改變後依次執行,其實這個過程就是觀察者模式
            self.onResolveCallbacks.push(function () {
                setTimeout(function () {
                    try{
                        onFulfilled(self.value);
                    }catch(e){
                        reject(e)
                    }
                })
            });
            self.onRejectCallbacks.push(function () {
                setTimeout(function () {
                    try{
                         onRejected(self.value);
                    }catch(e){
                        reject(e)
                    }
                })
            })
        });
    }
};
複製程式碼

以上只是支援了一次then的呼叫,而現實中我們會有這種需求

const p = new Promise(function(resolve,reject){
    if(/*非同步操作成功*/){
        resolve(value);
    }else{
        reject(error);
    }
});
p.then(function(){
    //success
}).then(function(){
    //success
}).then(function(){
    //success
}).catch(function(e){
    //failure
})
複製程式碼

這種連續鏈式呼叫then方法,連續返回promise例項的情況,而且我們要相容then方法裡返回的不是promise物件,這要求對then優化,加入一個解析promise的方法resolvePromise。這樣我們就會遇到三種情況:

  • 返回值是promise例項

  • 返回值是個thenable物件或者函式

  • 沒有返回值或者只返回一個普通值

    先看一下什麼是thenable物件

const thenable = {
    // 所謂 thenable 物件,就是具有 then 屬性,而且屬性值是如下格式函式的物件
    then: (resolve, reject) => {
        resolve(200)
    }
}
複製程式碼
//resolvePromise
function resolvePromise(promise2,x,resolve,reject){
    if(promise2 === x){
        return reject(new TypeError(`迴圈引用`))
    }
    let called = false;// 是否resolve或reject被呼叫,這兩個回撥只能被呼叫一次
    if(x instanceof Promise){
        if(x.status === PENDING){
            x.then(function (y) { // 深度遞迴
                resolvePromise(promise2,y,resolve,reject)
            },reject)
        }else{
            x.then(resolve,reject)
        }
    }else if(x != null && ((typeof x ==`object`) || (typeof x == `function`))){
            // x 是個thenable物件或函式
        try{
            let then = x.then;
            if(typeof then == `function`){
                then.call(x,function (y) {
                    // 如果promise2已經成功或者失敗了就不要再呼叫了
                    if(called) return;
                    called = true;
                    resolvePromise(promise2,y,resolve,reject)
                },function (err) {
                    if(called) return;
                    called = true;
                    reject(err)
                })
            }else{
                // 到此的話x不是個thenabe物件,直接把它當成值 resolve promise2就可以了
                resolve(x)
            }
        }catch (e) {
            if(called) return;
            called = true;
            reject(e)
        }
    }else{
        // 如果x是個普通值,則用x的值去resolve promise2
        resolve(x)
    }
}
複製程式碼

優化後的then方法

Promise.prototype.then = function (onFulfilled, onRejected) {
    // 如果成功和失敗的回撥沒傳,表示這個then沒任何邏輯,只會把值往後拋
    onFulfilled = typeof onFulfilled == `function` ? onFulfilled : value=>value;
    onRejected = typeof onRejected == `function` ? onRejected : reason=>{ throw reason};
    let self = this;
    let promise2;
    if(self.status == FULFILLED){ //如果promise狀態已經是成功態了,onFulfilled直接取值
        return promise2 = new Promise(function (resolve,reject) {
            setTimeout(function () {
                try{
                    let x = onFulfilled(self.value);
                    if(x instanceof Promise){
                        // 如果獲取到返回值X,會走解析promise的過程
                        resolvePromise(promise2,x,resolve,reject)
                    }
                }catch (e) {
                    // 如果執行成功的回撥過程中出錯,用錯誤原因把promise2 reject
                    reject(e)
                }
            })
        });
    }
    if(self.status == REJECTED){
        return promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try{
                    let x = onRejected(self.value);
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            })
        })
    }
    if(self.status == PENDING){
        return promise2 = new Promise(function (resolve, reject){
            self.onResolveCallbacks.push(function () {
                setTimeout(function () {
                    try{
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                })
            });
            self.onRejectCallbacks.push(function () {
                setTimeout(function () {
                    try{
                        let x = onRejected(self.value);
                        resolvePromise(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                })
            })
        });
    }
};
複製程式碼
  • catch方法

catch原理就是隻傳失敗的回撥

Promise.prototype.catch = function(onReject){
    this.then(null,onReject);
};
複製程式碼
  • 最後匯出這個類就可以了
try {
    module.exports = Promise
} catch (e) {
    console.log(e)
}
複製程式碼

實際專案中的應用—封裝一個非同步載入圖片的方法

function imgLoad(url) {
    return new Promise(function(resolve, reject) {
      var request = new XMLHttpRequest();
      request.open(`GET`, url);
      request.responseType = `blob`;
      request.onload = function() {
        if (request.status === 200) {
          resolve(request.response);
        } else {
          reject(Error(`Image didn`t load successfully; error code:` + request.statusText));
        }
      };
      request.onerror = function() {
          reject(Error(`There was a network error.`));
      };
      request.send();
    });
  }

  var body = document.querySelector(`body`);
  var myImage = new Image();

  imgLoad(`XXX.jpg`).then(function(response) {
    var imageURL = window.URL.createObjectURL(response);
    myImage.src = imageURL;
    body.appendChild(myImage);
  }, function(Error) {
    console.log(Error);
  });
複製程式碼

參考資料

MDN Promise

Promises/A+

相關文章