來,用ES6寫個Promise吧

tenor發表於2019-02-21

本文采用es6語法實現Promise基本的功能, 適合有javascript和es6基礎的讀者,如果沒有,請閱讀

  • http://es6.ruanyifeng.com/

回撥函式

在日常開發中,通常會使用ajax請求資料,拿到資料後,對資料進行處理。

但是,假設你需要多次ajax請求資料,並且每次ajax請求資料都是基於上一次請求資料作為引數,再次發出請求,於是程式碼可能就成了這樣:

function dataAjax() {
    $.ajax({
        url: `/woCms/getData`,
        type: `get`,
        success: function (data) {
            reChargeAjax(data);
        }
    });
}
function reChargeAjax(data) {
    $.ajax({
        url: `/woCms/reCharge`,
        type: `post`,
        data: { data },
        success: function (data) {
            loginAjax(data);
        }
    });
}
....
複製程式碼

很明顯,這樣的寫法有一些缺點:

  • 如果需求存在多個操作並且層層依賴的話,那這樣的巢狀就可能存在多層,並且每次都需要對返回的資料進行處理,這樣就嚴重加大了除錯難度,程式碼不是很直觀,並且增加了後期維護的難度
  • 這種函式的層層巢狀,又稱之為回撥地域,為了解決這種形式存在的缺點,並支援多個併發的請求,於是Promise就出現了

Promise是什麼

  • Promise是一種非同步流程的控制手段。
  • Promise物件能夠使我們更合理、更規範的處理非同步流程操作.

Promise基本用法

let pro = new Promise(function (resolve, reject) {
});
複製程式碼
  • Promise是一個全域性物件,也就是一個類
  • 可通過new關鍵詞建立一個Promise例項
  • Promise建構函式傳遞一個匿名函式, 匿名函式有2個引數: resolve和reject
  • 兩個引數都是方法,resolve方法處理非同步成功後的結果
  • reject處理非同步操作失敗後的原因

Promise的三種狀態

  • pending: Promise 建立完成後,預設的初始化狀態
  • fulfilled: resolve方法呼叫時,表示操作成功
  • rejected:reject方法呼叫時,表示操作失敗

狀態只能從初始化 -> 成功或者初始化 -> 失敗,不能逆向轉換,也不能在成功fulfilled 和失敗rejected之間轉換。

Promise 類構造

好了,目前我們知道了Promise的基礎定義和語法,那麼我們就用程式碼來模擬Promise的建構函式和內部實現吧

class Promise {
    // 建構函式,構造時傳入一個回撥函式
    constructor(executor) {
        this.status = "pending";// promise初始化狀態為pending
        this.value = undefined;// 成功狀態函式的資料
        this.reason = undefined;// 失敗狀態的原因

        // new Promise((resolve,reject)=>{});
        let resolve = data => {
            // 成功狀態函式,只有當promise的狀態為pending時才能修改狀態
            // 成功或者失敗狀態函式,只能呼叫其中的一個
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.value = data;
            }
        }
        let reject = err => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.reason = err;
            }
        }
    }
    executor(resolve, rejcte);
}
複製程式碼

在Promise的匿名函式中傳入resolve, rejcte這兩個函式,分別對應著成功和失敗時的處理,根據Promise規範,只有當Promise的狀態是初始化狀態是才能修改其狀態為成功或者失敗,並且只能轉為成功或者失敗。

then方法

在瞭解Promise建立和狀態之後,我們來學習Promise中一個非常重要的方法:then方法

then()方法:用於處理操作後的程式,那麼它的語法是什麼樣子的呢,我們一起來看下

pro.then(function (res) {
    //操作成功的處理
}, function (error) {
    //操作失敗的處理
}); 
複製程式碼

那麼then函式該如何定義呢?

很明顯,then屬於Promise原型上的方法,接收2個匿名函式作為引數,第一個是成功函式,第二個是失敗函式。

也就是說當Promise狀態變為成功的時候,執行第一個函式。當狀態變為失敗的時候,執行第二個函式。因此then函式可以這樣定義:

then(onFulFiled, onRejected) {
    // promise 執行返回的狀態為成功態
    if (this.status === "fulfilled") {
        // promise執行成功時返回的資料為成功態函式的引數
        onFulFiled(this.value);
    }
    if (this.status === "rejected") {
        // promise 執行失敗是返回的原因為失敗態函式的引數
        onRejected(this.reason);
    }
}
複製程式碼

Promise then 非同步程式碼佇列執行

但是我們現在來看這樣一段Promise程式碼:

let pro = new Promise((resolve, reject) => {
    setTimeout(function () {
        resolve("suc");
    }, 1000);
});
// 同一個promise例項多次呼叫then函式
pro.then(data => {
    console.log("date", data);
}, err => {
    console.log("err", err);
});
pro.then(data => {
    console.log("date", data);
}, err => {
    console.log("err", err);
})

輸出結果為
date suc
date suc
複製程式碼

這樣Promise中非同步呼叫成功或者失敗狀態函式,並且Promise例項多次呼叫then方法,我們可以看到最後輸出多次成功狀態函式中的內容,而此時:

  • Promise中非同步執行狀態函式,但Promise的then函式是還是pending狀態
  • Promise例項pro多次呼叫then函式,當狀態是pending的時候依然會執行狀態函式

那麼這塊在then函式中是如何處理pending狀態的邏輯呢?

採用釋出訂閱的方式,將狀態函式存到佇列中,之後呼叫時取出。

class Promise{
    constructor(executor){
        // 當promise中出現非同步程式碼將成功態或者失敗態函式封裝時
        // 採用釋出訂閱的方式,將狀態函式存到佇列中,之後呼叫時取出
        this.onFuiFiledAry = [];
        this.onRejectedAry = [];

        let resolve = data => {
            if (this.status === "pending") {
                ...
                // 當狀態函式佇列中有存放的函式時,取出並執行,佇列裡面存的都是函式
                this.onFulFiledAry.forEach(fn => fn());
            }
        }
        let reject = err => {
            if (this.status === "pending") {
                ...
                this.onRejectedAry.forEach(fn => fn());
            }
        }
    }
    then(onFuiFiled, onRejected) {
        ...
        if (this.status === "pending") {
            this.onFuiFiledAry.push(() => {
                onFuiFiled(this.value);
            });
            this.onRejectedAry.push(() => {
                onRejected(this.reason);
            });
        }
    }
}
複製程式碼

Promise then 函式返回一個新的Promise

看過jQuery原始碼的童鞋肯定都清楚,jQuery是如何實現鏈式呼叫的呢?對,就是this,jQuery通過在函式執行完成後通過返回當前物件的引用,也就是this來實現鏈式呼叫。

我們先看看一個例子,假設Promise用this來實現鏈式呼叫,會出現什麼情況呢?

let pro = new Promise(function (resolve, reject) {
    resolve(123);
});
let p2 = pro.then(function () {
    // 如果返回this,那p和p2是同一個promise的話,此時p2的狀態也應該是成功
    // p2狀態設定為成功態了,就不能再修改了,但是此時丟擲了異常,應該是走下個then的成功態函式
    throw new Error("error");
});
p2.then(function (data) {
    console.log("promise success", data);
}, function (err) {
    // 此時捕獲到了錯誤,說明不是同一個promise,因為promise的狀態變為成功後是不能再修改狀態
    console.log("promise or this:", err);
})
複製程式碼

很明顯,Promise發生異常,丟擲Error時,p2例項的狀態已經是失敗態了,所以會走下一個then的失敗態函式,而結果也正是這樣,說明Promise並不是通過this來實現鏈式呼叫。

那Promise中的鏈式呼叫是如何實現的呢?

結果是,返回一個新的Promise.

then(onFuiFiled, onRejected) {
    let newpro;
    if (this.status === "fulfilled") {
        newpro = new Promise((resolve, reject) => {
            onFuiFiled(this.value);
        });
    }
    if (this.status === "rejected") {
        newpro = new Promise((resolve, reject) => {
            onRejected(this.reason);
        });
    }
    if (this.status === "pending") {
        newpro = new Promise((resolve, reject) => {
            this.onFuiFiledAry.push(() => {
                onFuiFiled(this.value);
            });
            this.onRejectedAry.push(() => {
                onRejected(this.reason);
            });
        });
    }
    return newpro;
}
複製程式碼

Promise then函式返回值解析

好了,我們繼續將目光放在then函式上,then函式接收兩個匿名函式,那假設then函式返回的是數值、物件、函式,或者是promise,這塊Promise又是如何實現的呢?

來,我們先看例子:

let pro = new Promise((resolve, reject) => {
    resolve(123);
});
pro.then(data => {
    console.log("then-1",data);
    return 1;
}).then(data => {
    console.log("then-2", data);
});
複製程式碼

例子輸出的結果是
then-1 123
then-2 1

也就是說Promise會根據then狀態函式執行時返回的不同的結果來進行解析:

  • 如果上一個then函式返回的是數值、物件、函式的話,是會直接將這個數值和物件直接返回給下個then;
  • 如果then返回的是null,下個then獲取到的也是null;
  • 如果then返回的是一個新的promise的話,則根據新的promise的狀態函式來確定下個then呼叫哪個狀態函式,如果返回的新的promise沒有執行任何狀態函式的話,則這個promise的狀態是pending

那這塊,我們該如何實現呢?來,看具體程式碼吧

function analysisPromise(promise, res, resolve, reject) {
    // promise 中返回的是promise例項本身,並沒有呼叫任何的狀態函式方法
    if (promise === res) {
        return reject(new TypeError("Recycle"));
    }
    // res 不是null,res是物件或者是函式
    if (res !== null && (typeof res === "object" || typeof res === "function")) {
        try {
            let then = res.then;//防止使用Object.defineProperty定義成{then:{}}
            if (typeof then === "function") {//此時當做Promise在進行解析
                then.call(res, y => {
                    // y作為引數,promise中成功態的data,遞迴呼叫函式進行處理
                    analysisPromise(promise, y, resolve, reject);
                }, err => {
                    reject(err);
                })
            } else {
                // 此處then是普通物件,則直接呼叫下個then的成功態函式並被當做引數輸出
                resolve(res);
            }
        } catch (error) {
            reject(error);
        }
    } else {
        // res 是數值
        resolve(res);
    }
}
then(onFuiFiled, onRejected) {
    let newpro;
    if (this.status === "fulfilled") {
        newpro = new Promise((resolve, reject) => {
            let res = onFuiFiled(this.value);
            analysisPromise(newpro, res, resolve, reject);
        });
    }
    ...
    return newpro;
}
複製程式碼

analysisPromise函式就是用來解析Promise中then函式的返回值的,在呼叫then函式中的狀態函式返回結果值時都必須要進行處理。

Promise中的then函式是非同步

現在,我們再來看Promise的一個例子:

let pro = new Promise((resolve, reject) => {
    resolve(123);
});
pro.then(data => {
    console.log(1);
});
console.log(2);
複製程式碼

輸出的結果是
2
1

so,這裡先輸出的是2,而then函式中的狀態函式後輸出1,說明同步程式碼先於then函式的非同步程式碼執行

那這部分的程式碼我們該如何來實現呢?

採用setTimeout函式,給then函式中的每個匿名函式都加上setTimeout函式程式碼,就比如這樣:

then(onFuiFiled, onRejected) {
    let newpro;
    if (this.status === "fulfilled") {
        newpro = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let res = onFuiFiled(this.value);
                    analysisPromise(newpro, res, resolve, reject);
                } catch (error) {
                    reject(error);
                }   
            });
        });
    }
    ...
    return newpro;
}
複製程式碼

好了,關於Promise的大致實現就先分析到這裡,希望對大家有幫助,文章中如有錯誤,歡迎指正

文章主要參考一下具體學習資源

  • http://es6.ruanyifeng.com/
  • http://liubin.org/promises-book/#es6-promises

相關文章