ES6 Promise的使用和理解

zhangyuxiang1226發表於2018-07-31

JS的非同步

JS語言的執行環境是“單執行緒”的,即指一次只能完成一件任務;如果有多個任務,那麼必須排隊,前面一個任務完成,再執行後一個任務,以此類推。這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。

為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。

"同步模式"就是上一段的模式,後一個任務等待前一個任務結束,然後再執行,程式的執行順序與任務的排列順序是一致的、同步的;"非同步模式"則完全不同,每一個任務有一個或多個回撥函式(callback),前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步的。

"非同步模式"非常重要。在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,"非同步模式"甚至是唯一的模式,因為執行環境是單執行緒的,如果允許同步執行所有http請求,伺服器效能會急劇下降,很快就會失去響應。

常用的非同步程式設計模式

  1. 回撥函式
    即f1,f2兩個函式,f2要等待f1執行結果後執行,即 f1(f2)
  2. 事件驅動的方式
    f1.on('done', f2); (JQ寫法,f1完成時,trigger("done")則執行f2)
  3. 釋出-訂閱 模式
  4. Promise物件實現

Promise物件

本文著重講ES6的Promise物件的定義和用法 阮一峰老師的ES6詳解 - Promise物件 相信大家在學習ES6的過程中都或多或少的學習過阮老師的ES6教程,那麼這裡簡單舉一些例子講述Promise物件的特點和使用方法

基礎使用方法

ES6提供Promise建構函式,我們創造一個Promise例項,Promise建構函式接收一個函式作為引數,這個傳入的函式有兩個引數,分別是兩個函式 resolvereject作用是,resolve將Promise的狀態由未成功變為成功,將非同步操作的結果作為引數傳遞過去;相似的是reject則將狀態由未失敗轉變為失敗,在非同步操作失敗時呼叫,將非同步操作報出的錯誤作為引數傳遞過去。
例項建立完成後,可以使用then方法分別指定成功或失敗的回撥函式,比起f1(f2(f3))的層層巢狀的回撥函式寫法,鏈式呼叫的寫法更為美觀易讀

let promise = new Promise((resolve, reject)=>{
    reject("拒絕了");
});
promise.then((data)=>{
    console.log('success' + data);
}, (error)=>{
    console.log(error)
});

執行結果:"拒絕了"

複製程式碼

Promise的特點

  • 物件不受外界影響,初始狀態為pending(等待中),結果的狀態為resolve和reject,只有非同步操作的結果決定這一狀態
  • 狀態只能由pending變為另外兩種的其中一種,且改變後不可逆也不可再度修改,
    即pending -> resolved 或 pending -> reject
let promise = new Promise((resolve, reject)=>{
    reject("拒絕了");
    resolve("又通過了");
});
promise.then((data)=>{
    console.log('success' + data);
}, (error)=>{
    console.log(error)
});

執行結果: "拒絕了"

複製程式碼

上述程式碼不會再執行resolve的方法

then方法的規則

  • then方法下一次的輸入需要上一次的輸出
  • 如果一個promise執行完後 返回的還是一個promise,會把這個promise 的執行結果,傳遞給下一次then
  • 如果then中返回的不是Promise物件而是一個普通值,則會將這個結果作為下次then的成功的結果
  • 如果當前then中失敗了 會走下一個then的失敗
  • 如果返回的是undefined 不管當前是成功還是失敗 都會走下一次的成功
  • catch是錯誤沒有處理的情況下才會走
  • then中不寫方法則值會穿透,傳入下一個then

用node fs模組讀取檔案的流程來測試 我們建立一個讀取檔案的方法,在Promise中定義如果讀取成功則展示檔案的內容,否則報出錯誤

let fs = require('fs');

function read(file, encoding) {
    return new Promise((resolve, reject)=>{
        fs.readFile(filePath, encodeing, (err, data)=> {
            if (err) reject(err);
            resolve(data);
        });
    })
}
複製程式碼

由於想看到多次連貫回撥,我們專門設定3個txt檔案,其中1號檔案的內容為2號檔案的檔名,2號檔案的內容為3號檔案的檔名,3號中展示最終內容

ES6 Promise的使用和理解

執行程式碼如下:

read('1.promise/readme.txt', 'utf8').then((data)=>{
    console.log(data)
});
複製程式碼

讀取一個檔案的列印結果為,readme2.txt
我們改造這個程式碼,新增多個回撥,在最後一個之前的所有then中都return出當前返回的promise物件

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then((data)=>{
    console.log(data);
});

最終輸出 readme3.txt的內容

複製程式碼

ES6 Promise的使用和理解
ES6 Promise的使用和理解

再對下一步then進行新的處理,我們對readme3.txt的內容進行加工並返回

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then(data=>{
    return data.split('').reverse().join(); // 這一步返回的是一個普通值,普通值在下一個then會作為resolve處理
}).then(null,data=>{   // 特意不對成功做處理,放過這一個值,進入下一步
    throw new Error('出錯')  // 由於上一步返回的是普通值,走成功回撥,不會走到這裡
}).then(data=>{
    console.log(data)  // 最終會列印出來上上步的普通值
});
複製程式碼

這裡我們將內容處理後,則將一個普通值傳給了下次的then,但是由於下一個then沒有處理成功方法(null)

ES6 Promise的使用和理解
這個普通值會繼續傳入下一個then,最終會作為成功值列印出來。

最後我們看一下對錯誤的處理,在處理完readme3.txt的結果後,我們將這個值傳入下一個then中,令其作為一個檔名開啟,然而此時已經找不到這個不存在的檔案了,那麼在最後一步就會列印出報錯的結果

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then(data=>{
    return data.split('').reverse().join();
}).then(null,data=>{
    throw new Error('出錯')
}).then(data=>{
    return read(data, 'utf8')
}).then(null,(err)=>{
    console.log(err)
});
複製程式碼

結果:

ES6 Promise的使用和理解

Promises A+ (Promises Aplus)

Promises Aplus規範即規定了Promise的原理,原始碼規範等,通過這個規範,我們可以自己實現一個基於PromiseA+規範的Promise類庫,下面我們展示一下原始碼的實現

/**
 * Promise 實現 遵循promise/A+規範
 * 官方站: https://promisesaplus.com/
 * Promise/A+規範譯文:
 * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
 */

// promise 三個狀態
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let self = this; // 快取當前promise例項物件
    self.status = PENDING; // 初始狀態
    self.value = undefined; // fulfilled狀態時 返回的資訊
    self.reason = undefined; // rejected狀態時 拒絕的原因
    self.onFulfilledCallbacks = []; // 儲存fulfilled狀態對應的onFulfilled函式
    self.onRejectedCallbacks = []; // 儲存rejected狀態對應的onRejected函式

    function resolve(value) { // value成功態時接收的終值
        if(value instanceof Promise) {
            return value.then(resolve, reject);
        }

        // 為什麼resolve 加setTimeout?
        // 2.2.4規範 onFulfilled 和 onRejected 只允許在 execution context 棧僅包含平臺程式碼時執行.
        // 這裡的平臺程式碼指的是引擎、環境以及 promise 的實施程式碼。實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。

        setTimeout(() => {
            // 呼叫resolve 回撥對應onFulfilled函式
            if (self.status === PENDING) {
                // 只能由pedning狀態 => fulfilled狀態 (避免呼叫多次resolve reject)
                self.status = FULFILLED;
                self.value = value;
                self.onFulfilledCallbacks.forEach(cb => cb(self.value));
            }
        });
    }

    function reject(reason) { // reason為失敗態時接收的原因
        setTimeout(() => {
            // 呼叫reject 回撥對應onRejected函式
            if (self.status === PENDING) {
                // 只能由pedning狀態 => rejected狀態 (避免呼叫多次resolve reject)
                self.status = REJECTED;
                self.reason = reason;
                self.onRejectedCallbacks.forEach(cb => cb(self.reason));
            }
        });
    }

    // 捕獲在excutor執行器中丟擲的異常
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}


複製程式碼

這一部分程式碼我們對resolve和reject進行了判斷處理,接著我們構造then方法

/**
 * [註冊fulfilled狀態/rejected狀態對應的回撥函式]
 * @param  {function} onFulfilled fulfilled狀態時 執行的函式
 * @param  {function} onRejected  rejected狀態時 執行的函式
 * @return {function} promise2  返回一個新的promise物件
 */
Promise.prototype.then = function (onFulfilled, onRejected) {
    // 成功和失敗的回撥 是可選引數
    
    // onFulfilled成功的回撥 onRejected失敗的回撥
    let self = this;
    let promise2;
    // 需要每次呼叫then時都返回一個新的promise
    promise2 = new Promise((resolve, reject) => {
    // 成功態
        if (self.status === 'resolved') {
            setTimeout(()=>{
                try {
                    // 當執行成功回撥的時候 可能會出現異常,那就用這個異常作為promise2的錯誤的結果
                    let x = onFulfilled(self.value);
                    //執行完當前成功回撥後返回結果可能是promise
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        // 失敗態
        if (self.status === 'rejected') {
            setTimeout(()=>{
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'pending') {
           // 等待態時,當一部呼叫resolve/reject時,將onFullfilled/onReject收集暫存到集合中
           self.onResolvedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
        }
    });
    return promise2
}
// 其中規範要求對回撥中增加setTimeout處理

複製程式碼

可以看到resolve和reject都有一個處理新promise的方法resolvePromise,對其進行封裝,達到處理不同情況的目的

/**
 * 對resolve 進行改造增強 針對resolve中不同值情況 進行處理
 * @param  {promise} promise2 promise1.then方法返回的新的promise物件
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2,x,resolve,reject){
    if(promise2 === x){ // 如果從onFullfilled中返回的x就是promise2,就會導致迴圈引用報錯
        return reject(new TypeError('Chaining cycle'));
    }
    let called; // 宣告避免多次使用
    // x型別判斷 如果是物件或者函式
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
    // 判斷是否是thenable物件
        try{
            let then = x.then; 
            if(typeof then === 'function'){
                then.call(x,y=>{ 
                    if(called) return; 
                    called = true;
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ 
                    if(called) return;
                    called = true;
                    reject(err);
                });
            }else{
            // 說明是普通物件/函式
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true;
            reject(e);
        }
    }else{ 
        resolve(x);
    }
}
複製程式碼

以上基本實現了Promise的基本方法,根據Promise的用法,補充一些類上的方法

// 用於promise方法鏈時 捕獲前面onFulfilled/onRejected丟擲的異常
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason);
    })
}
Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        resolve(value);
    })
}
Promise.prototype.catch = function(onRejected){
    // 預設不寫成功
    return this.then(null,onRejected);
};
/**
 * Promise.all Promise進行並行處理
 * 引數: promise物件組成的陣列作為引數
 * 返回值: 返回一個Promise例項
 * 當這個陣列裡的所有promise物件全部變為resolve狀態的時候,才會resolve。
 */
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0;
        function processData(index,data){
            arr[index] = data;
            if(++i == promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
            promises[i].then(data=>{ // data是成功的結果
                processData(i,data);
            },reject);
        }
    })
}
/**
 * Promise.race
 * 引數: 接收 promise物件組成的陣列作為引數
 * 返回值: 返回一個Promise例項
 * 只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理(取決於哪一個更快)
 */
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i = 0;i<promises.length;i++){
            promises[i].then(resolve,reject);
        }
    })
}

複製程式碼

最後我們匯出方法

module.exports = Promise;
複製程式碼

至此一個符合PromiseA+規範的自己寫的原始碼庫完成了,可以測試使用這個庫替代Promise,以測試是否有邏輯錯誤等,或者可以使用

npm install promises-aplus-tests -g
promises-aplus-test 檔名
複製程式碼

外掛來測試該原始碼是否符合PromiseA+規範

希望這篇文章能幫到你,以上

相關文章