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

一.Promise的含義和意義

Promise是抽象異步處理對象以及對其進行各類操做的組件,其實Promise就是一個對象,用來傳遞異步操做的消息,它不是某門語言特有的屬性,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象,Promise對象有如下兩個特色:node

1.對象的狀態不受外界影響
2.一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果
git

Promise也如下缺點:es6

1.沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。
2.若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
3.當處於Pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。
github

關於Promise的詳細介紹和用法,能夠參考JavaScript Promise迷你書npm

2.爲何要在js中使用Promise
ES6新增了Promise這個特性的意義在於,以往在js中處理異步操做一般是使用回調函數和事件,而有了Promise對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易。拿node.js讀取文件舉例子,基於JavaScript的異步處理,以往都是想下面這樣利用回調函數:segmentfault

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中每每是這樣寫:promise

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
複製代碼
相關文章
相關標籤/搜索