用ES6實現一個簡單易懂的Promise(遵循Promise/A+ 規範並附詳解註釋)

nightZing發表於2018-01-15

一.Promise的含義和意義

Promise是抽象非同步處理物件以及對其進行各種操作的元件,其實Promise就是一個物件,用來傳遞非同步操作的訊息,它不是某門語言特有的屬性,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件,Promise物件有以下兩個特點:

1.物件的狀態不受外界影響
2.一旦狀態改變,就不會再變,任何時候都可以得到這個結果

Promise也以下缺點:

1.無法取消Promise,一旦新建它就會立即執行,無法中途取消。
2.如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
3.當處於Pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

關於Promise的詳細介紹和用法,可以參考JavaScript Promise迷你書

2.為什麼要在js中使用Promise
ES6新增了Promise這個特性的意義在於,以往在js中處理非同步操作通常是使用回撥函式和事件,而有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。拿node.js讀取檔案舉例子,基於JavaScript的非同步處理,以往都是想下面這樣利用回撥函式:

var fs = require('fs');
fs.readFile('demo.txt', 'utf8', function (err, data) {
          if (err) throw err;
         console.log(data);
});
複製程式碼

而使用Promise可以這樣寫:

var fs = require('fs');
function readFile(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}
readFile(('demo.txt').then(
    function(data) {
        console.log(data);
    }, 
    function(err) {
        throw err;
    }   
);
複製程式碼

這樣的結構就比較清晰了,有同學看到這要問了,要是有多重巢狀怎麼辦,來看下面這個例子,假如我們有多個延時任務要處理,在js中便使用setTimeout來實現,在以往就是js中往往是這樣寫:

var taskFun = function() {   
    setTimeout(function() {
               // do timeoutTask1
              console.log("do timeoutTask1");
        setTimeout(function() {
                   // do timeoutTask2
                  console.log("do timeoutTask2");
            setTimeout(function() {
                      // dotimeoutTask3
                     console.log("do timeoutTask3");
            }, 3000);
        }, 1000); 
    }, 2000);
}
 taskFun();
複製程式碼

這樣寫巢狀了多層回撥結構,如果業務邏輯再複雜一點,就會進入到所謂的回撥地獄,那麼如果用Promise可以這樣來寫:

new Promise(function(resolve, reject) {
    console.log("start timeoutTask1");
    setTimeout(resolve, 3000);
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask1");
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask2");
        setTimeout(resolve, 1000);
    });
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask2");
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask3");
        setTimeout(resolve, 2000);
    });
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask3");
});
複製程式碼

我們還可以用Promise這樣寫,把每個任務提煉成單獨函式,讓程式碼看起來更加優雅直觀:

function timeoutTask1() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask1");
        setTimeout(resolve, 3000);
    });
}

function timeoutTask2() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask2");
        setTimeout(resolve, 1000);
    });
}

function timeoutTask3() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask3");
        setTimeout(resolve, 2000);
    });
}

timeoutTask1()
    .then(function() {
        // do timeoutTask1
        console.log("do timeoutTask1");
    })
    .then(timeoutTask2)
    .then(function() {
        // do timeoutTask2
        console.log("do timeoutTask2");
    })
    .then(timeoutTask3)
    .then(function() {
        // do timeoutTask2
        console.log("do timeoutTask3");
    });
複製程式碼

執行的順序為:

執行結果

二.用ES6自己實現一個遵循Promise/A+規範的Promise

Promise/A+是Promise的一個主流規範,瀏覽器,node和JS庫依據此規範來實現相應的功能,以此規範來實現一個Promise也可以叫做實現一個Promise/A+。具體內容可參考Promise/A+規範

1.類和構造器的構建
Promise 的引數是一個函式 task,把內部定義 resolve 和reject方法作為引數傳到 task中,呼叫 task。當非同步操作成功後會呼叫 resolve 方法,然後就會執行 then 中註冊的回撥函式,失敗是呼叫reject方法。

class Promise {
    constructor(task) {
        let self = this; //快取this
        self.status = 'pending'; //預設狀態為pending
        self.value = undefined;  //存放著此promise的結果
        self.onResolvedCallbacks = [];  //存放著所有成功的回撥函式
        self.onRejectedCallbacks = [];   //存放著所有的失敗的回撥函式

        // 呼叫resolve方法可以把promise狀態變成成功態
        function resolve(value) {
            if (value instanceof Promise) {
                return value.then(resolve, reject)
            }
            setTimeout(() => { // 非同步執行所有的回撥函式
                // 如果當前狀態是初始態(pending),則轉成成功態
                // 此處這個寫判斷的原因是因為resolved和rejected兩個狀態只能由pending轉化而來,兩者不能相互轉化
                if (self.status == 'pending') {
                    self.value = value;
                    self.status = 'resolved';
                    self.onResolvedCallbacks.forEach(item => item(self.value));
                }
            });

        }

        // 呼叫reject方法可以把當前的promise狀態變成失敗態
        function reject(value) {
            setTimeout(() => {
                if (self.status == 'pending') {
                    self.value = value;
                    self.status = 'rejected';
                    self.onRejectedCallbacks.forEach(item => item(value));
                }
            });
        }

        // 立即執行傳入的任務
        try {
            task(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
}
複製程式碼

程式碼思路與要點:

  • self = this, 不用擔心this指向突然改變問題。
  • 每個 Promise 存在三個互斥狀態:pending、fulfilled、rejected。
  • Promise 物件的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和從 pending 變為 rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對 Promise 物件新增回撥函式,也會立即得到這個結果。這與事件完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
  • 建立 Promise 物件同時,呼叫其 task, 並傳入 resolve和reject 方法,當 task 的非同步操作執行成功後,就會呼叫 resolve,也就是執行 Promise .onResolvedCallbacks 陣列中的回撥,執行失敗時同理。
  • resolve和reject 方法 接收一個引數value,即非同步操作返回的結果,方便傳值。

2.Promise.prototype.then鏈式支援

 /**
     * onFulfilled成功的回撥,onReject失敗的回撥
     * 原型鏈方法
 */
    then(onFulfilled, onRejected) {
        let self = this;
        // 當呼叫時沒有寫函式給它一個預設函式值
        onFulfilled = isFunction(onFulfilled) ? onFulfilled : value => value;
        onRejected = isFunction(onRejected) ? onRejected : value => {
            throw value
        };
        let promise2;
        if (self.status == 'resolved') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        if (self.status == 'rejected') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        if (self.status == 'pending') {
            promise2 = new Promise((resolve, reject) => {
                self.onResolvedCallbacks.push(value => {
                    try {
                        let x = onFulfilled(value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                self.onRejectedCallbacks.push(value => {
                    try {
                        let x = onRejected(value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        return promise2;
    }
複製程式碼

程式碼思路與要點:

  • 呼叫 then 方法,將成功回撥放入 promise.onResolvedCallbacks 陣列;失敗回撥放入 promise.onRejectedCallbacks 陣列
  • 返回一個 Promise 例項 promise2,方便鏈式呼叫
  • then方法中的 return promise2 實現了鏈式呼叫
  • 如果傳入的是一個不包含非同步操作的函式,resolve就會先於 then 執行,即 promise.onResolvedCallbacks 是一個空陣列,為了解決這個問題,在 resolve 函式中新增 setTimeout,將 resolve 中執行回撥的邏輯放置到 JS 任務佇列末尾;reject函式同理。

3.靜態方法Promise.resolve

 static resolve(value) {
        return new Promise((resolve, reject) => {
            if (typeof value !== null && typeof value === 'object' && isFunction(value.then)) {
                value.then();
            } else {
                resolve(value);
            }
        })
    }
複製程式碼

靜態方法Promise.resolve(value) 可以認為是 new Promise() 方法的快捷方式。

比如 Promise.resolve(666); 可以認為是以下程式碼的語法糖。

new Promise(function(resolve){
    resolve(666);
});
複製程式碼

4.靜態方法Promise.reject

 static reject(err) {
        return new Promise((resolve, reject) => {
            reject(err);
        })
    }
複製程式碼

Promise.reject(err)是和 Promise.resolve(value) 類似的靜態方法,是 new Promise() 方法的快捷方式。

比如 Promise.reject(new Error("出錯了")) 就是下面程式碼的語法糖形式。

new Promise(function(resolve,reject){
    reject(new Error("出錯了"));
});
複製程式碼

4.靜態方法Promise.all

 /**
     * all方法,可以傳入多個promise,全部執行完後會將結果以陣列的方式返回,如果有一個失敗就返回失敗
     * 靜態方法為類自己的方法,不在原型鏈上
     */
    static all(promises) {
        return new Promise((resolve, reject) => {
            let result = []; // all方法最終返回的結果
            let count = 0; // 完成的數量
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(data => {
                    result[i] = data;
                    if (++count == promises.length) {
                        resolve(result);
                    }
                }, err => {
                    reject(err);
                });
            }
        });
    }
複製程式碼

Promise.all 接收一個 promise物件的陣列作為引數,當這個陣列裡的所有promise物件全部變為resolve或reject狀態的時候,它才會去呼叫 .then 方法。當全部為resolve時返回一個全部的resolve執行結果陣列,只要有一個不為resolve狀態,直接返回這個狀態的執行失敗結果。

5.靜態方法Promise.race

/**
     * race方法,可以傳入多個promise,返回的是第一個執行完的resolve的結果,如果有一個失敗就返回失敗
     *  靜態方法為類自己的方法,不在原型鏈上
*/
    static race(promises) {
        return new Promise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(data => {
                    resolve(data);
                },err => {
                    reject(err);
                });
            }
        });
    }
複製程式碼

Promise.race 和 Promise.all 相類似,它同樣接收一個陣列,race的意思是競賽,顧名思義只要是競賽就有唯一的那個第一名,所以它與all最大的不同是隻要該陣列中的任意一個 Promise 物件的狀態發生變化(無論是 resolve 還是 reject)該方法都會返回,所以它只輸出某一個最先執行的狀態結果,而不是像all一樣在全部為resolve狀態時返回的是一個陣列。只需在Promise.all 方法基礎上修改一下就可實現race。

三.總結

原始碼 以上是對幾個主要方法的介紹,還有些沒有介紹完全,可以參考原始碼,原始碼檔案裡包含了一個測試資料夾以及es5的版本原始碼,後續會奉上更為詳盡的解釋。另外可以通過安裝一個外掛來對實現的promise進行規範測試。

npm(cnpm) i -g promises-aplus-tests
promises-aplus-tests es6Promise.js
複製程式碼

相關文章