【你應該掌握的】Promise基礎知識&如何實現一個簡單的Promise

團隊:skFeTeam  本文做者:高揚前端

Promise是前端基礎技能中較爲重要的一部分,本文將從如下幾個方面展開Promise的相關知識,與你們交流。

  • 什麼是Promise
  • Promise能夠解決什麼問題
  • 該類問題是否有其餘解決方案
  • 如何使用Promise
  • 如何本身實現Promise

什麼是Promise?

Promise是一種異步編程的解決方案,已經被歸入ES6規範當中。在Promise出現以前,傳統的異步編程方案主要是點擊事件以及回調函數node

Promise能夠解決什麼問題?

簡單來講,Promise能夠避免出現回調地獄git

什麼是回調地獄?

JQuery中發起一個異步請求能夠寫爲:es6

$.ajax({
    type: 'GET',
    url: 'xxx',
    ...,
    success:function (data) {
        ...
    }
})
複製代碼

若是業務須要擴展,在獲取到請求結果後再發起一個異步請求,則代碼擴展爲:github

$.ajax({
    type: 'GET',
    url: 'xxx',
    ...,
    success:function (data1) {
        // 另外一個異步請求
        $.ajax({
            url: 'xxx',
            success: function (data2) {
                ...
            }
        })
    }
})
複製代碼

若是業務更加複雜,須要依次執行多個異步任務,那麼這些異步任務就會一層一層嵌套在上一個異步任務成功的回調函數中,咱們稱之爲回調地獄,代碼片斷以下。面試

// 第一個異步請求
$.ajax({
    url: 'x',
    success:function (data1) {
        // 第二個異步請求
        $.ajax({
            url: 'xx',
            success: function (data2) {
                // 第三個異步請求
                $.ajax({
                    url:'xxx',
                    success: function (data3) {
                        // 第四個異步請求
                        $.ajax({
                            url: 'xxxx',
                            success: function (data4) {
                                // 第五個異步請求
                                $.ajax({
                                    url: 'xxxxx',
                                    success: function (data5) {
                                        // 第N個回調函數
                                        ...
                                    }
                                })
                            }
                        })
                    }
                })
            }
        })
    }
})
複製代碼

回調地獄會形成哪些問題?

  • 代碼可讀性差
  • 業務耦合度高,可維護性差
  • 代碼臃腫
  • 代碼可複用性低
  • 排查問題困難

由於Promise能夠避免回調地獄的出現,所以以上問題也是Promise能夠解決的問題。ajax

該問題還有其餘解決方案嗎?

Promise規範推出後,基於該規範產生了許多回調地獄的解決方案,包括ES6原生Promise,bluebird,Q,then.js等。編程

此處可參考知乎nodejs異步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何優缺點?最愛哪一個?哪一個簡單? 再也不贅述。數組

Promise如何使用?

構造函數及API

一個完整的Promise對象包括如下幾個部分:promise

new Promise(function(resolve,reject) {
    ...
    resolve('success_result');
}).then(function (resolve) {
    console.log(resolve); // success_result
}).catch(function (reject) {
    console.log(reject);
});
複製代碼

對象聲明主體:方法主體,發起異步請求,返回的成功結果用resolve包裹,返回的失敗結果用reject包裹。
then:異步請求成功的回調函數,能夠接收一個參數,即異步請求成功的返回結果,或不接收參數。
catch:異步請求失敗的回調函數,處理捕獲的異常或異步請求失敗的後續邏輯,至多接收一個參數,即失敗的返回結果。

每一個Promise對象包含三種狀態:

  • pending:初始狀態
  • fulfilled/resolved:操做成功
  • rejected:操做失敗

Promise對象的狀態沒法由外界改變,且當狀態變化爲fulfilled/resolved或者rejected時,不會再發生變動。

咱們也能夠構造一個特定狀態的Promise對象,如

let fail = Promise.reject('fail');

let success = Promise.resolve(23);
複製代碼

不經常使用API之Promise.all()
將多個Promise對象包裝成一個Promise,若是所有執行成功,則返回全部成功結果的數組,若是有任務執行失敗,則返回最早失敗的Promise對象的返回結果。
示例:

let p1 = new Promise(function (resolve, reject) {
  resolve('成功');
});

let p2 = new Promise(function (resolve, reject) {
  resolve('success');
});

let p3 = Promse.reject('失敗');

Promise.all([p1, p2]).then(function (result) {
  console.log(result); // ['成功', 'success']
}).catch(function (error) {
  console.log(error);
});

Promise.all([p1,p3,p2]).then(function (result) {
  console.log(result);
}).catch(function (error) {
  console.log(error);  // '失敗'
})
複製代碼

不經常使用API之Promise.race()
多個異步任務同時執行,返回最早執行結束的任務的結果,不管成功仍是失敗。
示例:

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve('success');
  },1000);
});

let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    reject('failed');
  }, 500);
});

Promise.race([p1, p2]).then(function (result) {
  console.log(result);
}).catch(function (error) {
  console.log(error);  // 'failed'
});
複製代碼

Promise支持鏈式調用

Promise的then方法中容許追加新的Promise對象。
所以回調地獄能夠改寫爲:

var p1 = new Promise(function (resolve, reject) {
    ...
    resolve('success1');
});

var p2 = p1.then(function (resolve1) {
    ...
    console.log(resolve1); // success1
    resolve('success2');
});

var p3 = p2.then(function (resolve2) {
    console.log(resolve2); // success2
    resolve('success3');
});

var p4 = p3.then(...);

var p5 = p4.then(...);
複製代碼

也能夠簡寫爲:

new Promise(function (resolve, reject) {
    resolve('success1');
}).then(function (resolve1) {
    console.log(resolve1); // success1
    resolve('success2');
}).then(function (resolve2) {
    console.log(resolve2); // success2
    resolve('success3');
}).then(...);
複製代碼

以上邏輯均表示當接收到上一個異步任務返回的「success${N}」結果以後,纔會執行下一個異步任務。
鏈式調用的一個特殊狀況是透傳,Promise也是支持的,由於不管當前then方法有沒有接收到參數,都會返回一個Promise,這樣才能夠支持鏈式調用,纔會有下一個then方法。

let p = new Promise(function (resolve, reject) {
    resolve(1);
});

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

Promise在事件循環中的執行過程?

Promise在初始化時,代碼是同步執行的,即前文說起的對象聲明主體部分,而在then中註冊的回調函數是一個微任務,會在瀏覽器清空微任務隊列時執行。

關於瀏覽器中的事件循環請參考宏任務與微任務

Promise升級之async/await的執行過程

ES6中出現的async/await也是基於Promise實現的,所以在考慮async/await代碼在事件循環中的執行時機時仍然參考Promise。

function func1() {
    return 'await';
};
let func2 = async function () {
    let data2= await func1();
    console.log('data2:', data2);
};
複製代碼

以上代碼能夠用Promise改寫爲:

let func1 = Promise.resolve('await');

let func2 = function (data) {
    func1.then(function (resolve) {
        let data2 = resolve;
        console.log('data2:', data2);
    });
};

複製代碼

從改寫後的Promise能夠看出 console.log('data2:', data2) 在微任務隊列裏,所以改寫前的 console.log('data2:', data2) 也是在微任務隊列中。

由此可推斷出下列代碼片斷中

function func1() {
    console.log('func1');
};
let func2 = async function () {
    let data = await func1();
    console.log('func2');
}
複製代碼

console.log('func2') 也是微任務。

如何手寫一個Promise?

首先,Promise對象包含三種狀態,pending,fulfilled/resolved,rejected,而且pending狀態可修改成fulfilled/resolved或者rejected,此外咱們還須要一個變量存儲異步操做返回的結果,所以能夠獲得如下基本代碼。

// 定義Promise的三種狀態
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

function Promise(executor) {
    this.state = PENDING;
    this.value = undefined; // 用於存儲異步操做的返回結果

    /**
     * 異步操做成功的回調函數
     * @param {*} value 異步操做成功的返回結果
     */
    function resolve(value) {

    }

    /**
     * 異步操做失敗的回調函數
     * @param {*} value 異步操做失敗的拋出錯誤
     */
    function reject(value) {

    }

}

module.exports = Promise;
複製代碼

爲了加強代碼的可讀性咱們把三種狀態定義爲常量。

每個Promise對象都須要提供一個then方法用於處理異步操做的返回值。咱們將它定義在原型上。

Promise.prototype.then = function (onFulfilled, onRejected) {
    console.log('then'); // 測試語句
};
複製代碼

此時咱們寫一段代碼來測試這個Promise

let p = new Promise((resolve, reject) => {
    console.log('p');
});

p.then(() => {
    console.log('p then');
});
複製代碼

輸出

then
複製代碼

由於咱們如今尚未對聲明Promise對象以及then方法的入參作任何處理,所以pp then都不會打印。
首先咱們給Promise的聲明中增長代碼執行入參。

function Promise(executor) {
    this.state = PENDING;
    this.value = undefined; // 用於存儲異步操做的返回結果

    executor(resolve, reject); // 馬上執行

    /**
     * 異步操做成功的回調函數
     * @param {*} value 異步操做成功的返回結果
     */
    function resolve(value) {
    }
    /**
     * 異步操做失敗的回調函數
     * @param {*} value 異步操做失敗的拋出錯誤
     */
    function reject(value) {
    }
};
複製代碼

此時測試代碼輸出爲

p
then
複製代碼

接下來咱們來完善resolve和reject方法。由於Promise狀態只能夠由pending變化爲resolved或者rejected,且變化後就不能夠再變動。所以代碼可擴充爲:

function Promise(executor) {
    var _this = this;
    this.state = PENDING;
    this.value = undefined; // 用於存儲異步操做的返回結果 成功與失敗共用一個變量,也能夠選擇分開

    executor(resolve, reject); // 馬上執行

    /**
     * 異步操做成功的回調函數
     * @param {*} value 異步操做成功的返回結果
     */
    function resolve(value) {
        if(_this.state === PENDING) {
            _this.value = value;
            _this.state = RESOLVED;
        }
    }

    /**
     * 異步操做失敗的回調函數
     * @param {*} value 異步操做失敗的拋出錯誤
     */
    function reject(value) {
        if(_this.state === PENDING) {
            _this.value = value;
            this.state = REJECTED;
        }
    }
};
複製代碼

接下來完善then方法,成功時調用註冊的成功回調函數,失敗時調用註冊的失敗回調函數。

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === RESOLVED) {
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }
    }

    if (this.state === REJECTED) {
        if (typeof onRejected === 'function') {
            onRejected(this.value);
        }
    }
};
複製代碼

考慮到後續代碼邏輯會複雜化,爲了減小在各個條件下都去判斷onFulfilled和onRejected是不是一個方法的重複代碼,代碼可再次優化爲:

Promise.prototype.then = function (onFulfilled, onRejected) {

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (onFulfilled) => onFulfilled;
    onRejected = typeof onRejected === 'function' ? onRejected : (onRejected) => {
        throw onRejected;
    };

    if (this.state === RESOLVED) {
        onFulfilled(this.value);
    }

    if (this.state === REJECTED) {
        onRejected(this.value);
    }
};
複製代碼

此時修改測試代碼爲

let p = new Promise((resolve, reject) => {
    console.log('p');
    resolve('success');
});

p.then((value) => {
    console.log('p then', value);
});
複製代碼

輸出

p
then
p then success
複製代碼

可是此時咱們手寫的Promise還不支持異步操做,運行以下測試代碼

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 500);
});

p.then((value) => {
    console.log('p then', value);
});
複製代碼

會發現p then 1並不會輸出。這是由於setTimeout使得resolve延遲執行,因此當運行then方法時,state尚未變動爲resolved,因此也不會調用onFulfilled方法。
爲了解決這個問題,咱們能夠爲成功的回調函數和失敗的回調函數各創建一個數組,當執行到then方法時若對象狀態尚未發生變化,就將回調函數寄存在數組中,等到狀態發生改變後再取出執行。
首先,須要新增兩個數組保存回調函數。

function Promise(executor) {
    var _this = this;
    this.state = PENDING;
    this.value = undefined; // 用於存儲異步操做的返回結果 成功與失敗共用一個變量,也能夠選擇分開
    this.onFulfilledFunc = []; // 保存成功的回調函數
    this.onRejectedFunc = []; // 保存失敗的回調函數

    executor(resolve, reject); // 馬上執行
    ...
};
複製代碼

而後,咱們在then方法中增長邏輯,若當前Promise對象還處於pending狀態,將回調函數保存在對應數組中。

Promise.prototype.then = function (onFulfilled, onRejected) {
    ...
    if (this.state === PENDING) {
        this.onFulfilledFunc.push(onFulfilled);
        this.onRejectedFunc.push(onRejected);
    }
    
    if (this.state === RESOLVED) {
        ...
    }

    if (this.state === REJECTED) {
        ...
    }
};
複製代碼

保存好回調函數後,當狀態改變,依次執行回調函數。

/**
 * 異步操做成功的回調函數
 * @param {Function} value 異步操做成功的返回結果
 */
function resolve(value) {
    if(_this.state === PENDING) {
        _this.value = value;
        _this.onFulfilledFunc.forEach(fn => fn(value));
        _this.state = RESOLVED;
    }
}

/**
 * 異步操做失敗的回調函數
 * @param {Function} value 異步操做失敗的拋出錯誤
 */
function reject(value) {
    if(_this.state === PENDING) {
        _this.value = value;
        _this.onRejectedFunc.forEach(fn => fn(value));
        this.state = REJECTED;
    }
}
複製代碼

此時從新執行測試代碼,輸出了p then 1,至此,咱們已經支持了Promise的異步執行。 接下來咱們再運行一段代碼來測試一下Promise的鏈式調用。

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 500);
});

p.then((value) => {
    console.log('p then', value);
    resolve(2);
}).then((value) => {
    console.log('then then ', value);
});
複製代碼

會發現不只沒有輸出正確的結果,控制檯還有報錯。
支持鏈式調用的核心在於,每一次調用都會返回一個Promise,這樣才能支持下一個then方法的調用。
其次,爲了支持Promise的鏈式調用,須要遞歸比較先後兩個Promise並按不一樣狀況處理,此時咱們須要分幾種狀況去考慮:

  • 當前then方法resolve的就是一個Promise -> 直接返回
  • 當前then方法resolve的是一個常量 -> 包裝成Promise返回
  • 當前then方法沒有resolve -> 視爲undefined包裝成Promise返回
  • 當前then方法既沒有入參也沒有resolve -> 繼續向下傳值,支持透傳
  • 當前then方法執行出現異常 -> 調用reject方法並傳遞給下一個then的reject

接下來咱們來改寫then方法。

Promise.prototype.then = function (onFulfilled, onRejected) {

    let self = this;
    let promise2; // 用於保存最終須要return的promise對象

    if (this.state === RESOLVED) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    if (this.state === REJECTED) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    if (this.state === PENDING) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    return promise2;
};
複製代碼

抽取出獨立的遞歸函數處理then方法。

/**
 * 根據上一個對象的then返回一個新的Promise
 * @param {*} promise 
 * @param {*} x 上一個then的返回值
 * @param {*} resolve 新的promise的resolve
 * @param {*} reject 新的promise的reject
 */
function resolvePromise(promise, x, resolve, reject) {
    if (promise === x && x !== undefined) {
        reject(new TypeError('發生了循環引用'));
    }
    if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
        // 對象或函數
        try {
            let then  = x.then;
            if (typeof then === 'function') {
                then.call(x, (y) => {
                    // resolve(y);
                    // 遞歸調用
                    resolvePromise(promise, y, resolve, reject);
                }, (e) => {
                    reject(e);
                })
            } else {
                resolve(x);
            }
        } catch (error) {
            // 若是拋出異常,執行reject
            reject(error);
        }

    } else {
        // 常量等
        resolve(x);
    }
}
複製代碼

在then方法中補充完整邏輯並增長setTimeout支持異步:

Promise.prototype.then = function (onFulfilled, onRejected) {

    let self = this;

    let promise2; // 用於保存最終須要return的promise對象
    ...

    if (this.state === RESOLVED) {
        promise2 = new Promise((resolve, reject) => {
            // 異步執行
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (error) {
                    reject(error);
                }
            })
        })
    }

    if (this.state === REJECTED) {
        promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (error) {
                    reject(error)
                }
            })
        })
    }

    if (this.state === PENDING) {
        promise2 = new Promise((resolve, reject) => {
            self.onFulfilledFunc.push(() => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                })
            });

            self.onRejectedFunc.push(() => {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                })
            })
        })
    }
    return promise2;
};
複製代碼

至此,咱們手寫的Promise就基本可使用了。

以上是對Promise相關知識的一些整理,其中瀏覽器的事件循環以及手寫Promise也是前端面試中比較重要的考察點,若有錯誤,歡迎指正。

參考連接

[1] Promise精選面試題
[2] 理解和使用Promise.all和Promise.race
[3] Promise API
[4] 只會用?一塊兒來手寫一個合乎規範的Promise
[5] 手寫Promise
[6] 【翻譯】Promises/A+規範
[7] promise by yuet
[8] [es6-design-mode by Cheemi](

想了解skFeTeam更多的分享文章,能夠點這裏,謝謝~

相關文章
相關標籤/搜索