ES6 Promise - 讓我們解開的面紗(遵循Promise/A+規範)

草履蟲的思考發表於2018-05-28
Promise 物件代表了未來將要發生的事件,用來傳遞非同步操作的訊息。ECMAscript 6 原生提供了 Promise 物件。而且ES7中的async/await也是Promise基礎實現的。Promise到底有魅力和作用呢?本文將解開它的面紗,探索promise的原理及用法。不用再被一些回撥地獄、各種非同步呼叫所頭疼煩惱。


什麼是Promise?

     promise,是承諾的意思。在JavaScript中promise指一個的物件或函式(是一個包含了相容promise規範then方法的物件或函式)。 Promise 物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise 物件提供統一的介面,使得控制非同步操作更加容易。
  1. promise的特點

1-1、 Promise的三種狀態   

  • pending: Promise物件例項建立時候的初始狀態
  • resolved:可以理解為成功的狀態
  • rejected:可以理解為失敗的狀態      
  1-2、 根據Promise/A+規範,可知
  • 如果是pending狀態,則promise:可以轉換到resolved或rejected狀態。
  • 如果是resolved狀態,則promise:不能轉換成任何其它狀態。必須有一個值,且這個值不能被改變。
  • 如果是rejected狀態,則promise可以:不能轉換成任何其它狀態。必須有一個原因,且這個值不能被改變。
”值不能被改變”指的是其identity不能被改變,而不是指其成員內容不能被改變。


1-3、promise 有一個 then 方法,就是用來指定Promise 物件的狀態改變時確定執行的操作,resolve 時執行第一個函式 (onFulfilled),reject 時執行第二個函式(onRejected)

promiseFn().then(resolve(onFulfilled){
        //當promise狀態變成fulfilled時,呼叫此函式
    },reject(onRejected){
        //當promise狀態變成rejected時,呼叫此函式
    });複製程式碼
  •  resolve,reject 都是可選引數
  1.  如果onFulfilled不是一個函式,則忽略之。
  2. 如果onRejected不是一個函式,則忽略之。
  • 如果onFulfilled是一個函式:

    1. 它必須在promise fulfilled後呼叫, 且promise的value為其第一個引數。
    2. 它不能在promise fulfilled前呼叫。
    3. 不能被多次呼叫。
  • 如果onRejected是一個函式,

    1. 它必須在promise rejected後呼叫, 且promise的reason為其第一個引數。
    2. 它不能在promise rejected前呼叫。
    3. 不能被多次呼叫。
  • onFulfilledonRejected 只允許在 execution context 棧僅包含平臺程式碼時執行. 
  • onFulfilledonRejected 必須被當做函式呼叫 (i.e. 即函式體內的 thisundefined). 
  • 對於一個promise,它的then方法可以呼叫多次.
    1. promise fulfilled後,所有onFulfilled都必須按照其註冊順序執行。
    2. promise rejected後,所有OnRejected都必須按照其註冊順序執行。
  • then 必須返回一個promise .

    promise2 = promise1.then(onFulfilled, onRejected);
    複製程式碼
    1. 如果onFulfilledonRejected 返回了值x, 則執行Promise 解析流程[[Resolve]](promise2, x).
    2. 如果onFulfilledonRejected丟擲了異常e, 則promise2應當以ereason被拒絕。
    3. 如果 onFulfilled 不是一個函式且promise1已經fulfilled,則promise2必須以promise1的值fulfilled.
    4. 如果 OnReject 不是一個函式且promise1已經rejected, 則promise2必須以相同的reason被拒絕.

 

2、為什麼使用promise?有什麼好處呢?

  • 2-1 對於回撥函式:  可以解決回撥地獄,如下圖:         ES6 Promise - 讓我們解開的面紗(遵循Promise/A+規範)例如,使用jQuery的ajax多次向後臺請求資料時,並且每個請求之間需要相互依賴,則需要回撥函式巢狀來解決而形成“回撥地獄”。

    $.get(url1, data1 => {
        console.log(data1,"第一次請求");
        $.get(data1.url, data2 => { // 第一次請求後的返回url 在此請求後臺
            console.log(data2,"第二次請求")
            .....
        })
    })
    
    
    複製程式碼
這樣一來,在處理越多的非同步邏輯時,就需要越深的回撥巢狀,
複製程式碼

這種編碼模式的問題主要有以下幾個: 

  1. 程式碼邏輯書寫順序與執行順序不一致,不利於閱讀與維護。
  2.  非同步操作的順序變更時,需要大規模的程式碼重構。
  3.  回撥函式基本都是匿名函式,bug 追蹤困難。 
  4. 回撥函式是被第三方庫程式碼(如上例中的 ajax )而非自己的業務程式碼所呼叫的,造成了 IoC 控制反轉。
  5. 結果不能通過return返回

Promise 怎麼解決呢?

let p = url1 => { 
    return new Promise((resolve, reject) => {
        $.get(url, data => {
            resolve(data)
        });
    })
};

// 
p(url).then(resvloe => {
    return p(resvloe.url);   
}).then(resvloe2 => {
    return resvloe2(resvloe2.url);
}).then(resvloe3 => {
    console.log(resvloe3);
}).catch(err => throw new Error(err));
當第一個then中返回一個promise,會將返回的promise的結果,傳遞到下一個then中。
這就是比較著名的鏈式呼叫了。   複製程式碼
  • 2-2、Promise 也有一些缺點。
  • 首先,無法取消 Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反應到外部。第三,當處於 Pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

3、Promise 使用

  • 1. 建立Promise。要想建立一個 promise 物件、可以使用 new 來呼叫 Promise 的構造器來進行例項化。接收一個excutor執行函式作為引數, excutor有兩個函式型別形參resolve reject。

let promise = new Promise(function(resolve, reject) {
    // 非同步處理
    // 處理結束後、呼叫resolve 或 reject
});
複製程式碼

  • 2.promise中的狀態變化。
1. promise 物件初始化狀態為 pending 
2. 當呼叫resolve(成功),會由pending => resolved 
3. 當呼叫reject(失敗),會由pending => rejected
  • 3. promise物件方法 then方法
3-1.then方法  註冊:對於已經例項化過的 promise 物件可以呼叫 promise.then() 方法,傳遞 resolve 和 reject 方法作為回撥。promise.then() 是 promise 最為常用的方法。

   // onFulfilled 是用來接收promise成功的值
  // onRejected 是用來接收promise失敗的原因 
// then方法是非同步的
promise.then(onFulfilled, onRejected);複製程式碼

3-2. resolve(成功): onFulfilled會被呼叫

let promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled' 
}, reason => { // onRejected 不會被呼叫
    
})
複製程式碼

3-3.reject(失敗) onRejected會被呼叫

let promise = new Promise((resolve, reject) => {
   reject('rejected'); // 狀態由 pending => rejected
});
promise.then(result => { // onFulfilled 不會被呼叫
  
}, reason => { // onRejected 
    console.log(reason); // 'rejected'
})
複製程式碼

3-4.promise.catch 方法:捕捉錯誤 

(catch 方法是 promise.then(null, rejection) 的別名,用於指定發生錯誤時的回撥函式。)複製程式碼

promise.catch(onRejected)
相當於
promise.then(null, onRrejected);

// 注意
// onRejected 不能捕獲當前onFulfilled中的異常
promise.then(onFulfilled, onRrejected); 

// 可以寫成:
promise.then(onFulfilled)
       .catch(onRrejected);   複製程式碼

3-5、promise chain  方法每次呼叫 都返回一個新的promise物件 所以可以鏈式寫法
Promise的靜態方法

function step1() {
    console.log("step1");
}
function step2() {
    console.log("step2");
}
function onRejected(error) {
    console.log("錯誤方法:", error);
}

var promise = Promise.resolve();
promise
    .then(step1)
    .then(step2)
    .catch(onRejected) // 捕獲前面then方法中的異常複製程式碼

4、Promise的方法是使用

  1. Promise.resolve 返回一個fulfilled狀態的promise物件
Promise.resolve('成功');
// 相當於
let promise = new Promise(resolve => {
   resolve('成功');
});
複製程式碼

//庫中實現
Promise.resolve = function (val) {  return new Promise((resolve, reject) => resolve(val))}
複製程式碼

2.Promise.reject 返回一個rejected狀態的promise物件

var p = Promise.reject('出錯了');
 
p.then(null, function (s){
  console.log(s)
});
// 出錯了
複製程式碼

3.Promise.all 接收一個promise物件陣列為引數

只有全部為resolve才會呼叫 通常會用來處理 多個並行非同步操作

const p1 = new Promise((resolve, reject) => {
    resolve(1);
});

const p2 = new Promise((resolve, reject) => {
    resolve(2);
});

const p3 = new Promise((resolve, reject) => {
    reject(3);
});

Promise.all([p1, p2, p3]).then(data => { 
    console.log(data); // [1, 2, 3] 結果順序和promise例項陣列順序是一致的
}, err => {
    console.log(err);
});
複製程式碼

//庫中實現
Promise.all = function(arr) {    
 return new Promise((resolve, reject) => {  
      let num = 0,innerArr = [];
       function done(index,data){   
         innerArr[index] = data;    
        num ++;           
 if(num === arr.length){    
            resolve(innerArr); 
           }                   }
      for(let i =0 ;i<arr.length;i++){     
    arr[i].then((res)=>{    
       done(i,res);       
  },reject); // 有一個失敗 就返回  
    }  
  })}
複製程式碼


4.Promise.race 接收一個promise物件陣列為引數

Promise.race 只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理。

function timerPromisefy(delay) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
var startDate = Date.now();

Promise.race([
    timerPromisefy(10),
    timerPromisefy(20),
    timerPromisefy(30)
]).then(function (values) {
    console.log(values); // 10
});
複製程式碼

// 庫中實現
Promise.race = function(arr) {  
  return new Promise((resolve, reject) => {   
     arr.forEach((item, index) => {
            item.then(resolve, reject); 
       }); 
   });}
複製程式碼

5.Promise的finally

Promise.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};複製程式碼

5、prmoise 程式碼庫實現


/** * Promise 實現 遵循promise/A+規範 * Promise/A+規範譯文: * https://promisesaplus.com/ */

// 判斷x是不是promise 根據規範
function resolvePromise(promise2, x, resolve, reject) {
    // 1、如果promise2 和 x 指向相同的值, 使用 TypeError做為原因將promise拒絕。 (就會導致迴圈引用報錯)   
 if (promise2 === x) { 
       return reject(new TypeError('迴圈引用')); 
   }  
  // 避免多次呼叫   
 let isUsed = false;
    // 2、如果x是一個promise物件 (該判斷和下面 判斷是不是thenable(thenable 是一個包含了then方法的物件或函式)物件重複 所以可有可無)     
/**     * 如果x是pending狀態,promise必須保持pending走到x resolved或rejected.        
如果x是resolved狀態,將x的值用於resolve promise.        如果x是rejected狀態, 將x的原因用於reject promise..  
   */  
  // if (x instanceof Promise) { // 獲得它的終值 繼續resolve   
 //     if (x.status === 'pending') { // 如果為等待態需等待直至 x 被執行或拒絕 並解析y值 遞迴   
 //         x.then(y => {   
 //             resolvePromise(promise2, y, resolve, reject); 
   //         }, reason => {  
  //             reject(reason);    
//         });    
//     } else { 
// 如果 x 已經處於執行態/拒絕態(值已經被解析為普通值),用相同的值執行傳遞下去 promise   
 //         x.then(resolve, reject); 
   //     }    
//    
 // 3、如果 x 為物件或者函式    
//     
/**    
//      *  1.將 then 賦為 x.then.  
   //         2.如果在取x.then值時丟擲了異常,則以這個異常做為原因將promise拒絕。  
  //         3.如果 then 是一個函式, 以x為this呼叫then函式, 且第一個引數是resolve,第二個引數是reject,且:
    //            3-1.當 resolve 被以 y為引數呼叫, 執行 [[Resolve]](promise, y).  
  //            3-2.當 reject 被以 reason 為引數呼叫, 則以reason為原因將promise拒絕。
    //            3-3.如果 resolvee 和 reject 都被呼叫了,或者被呼叫了多次,則只第一次有效,後面的忽略。
    //            3-4.如果在呼叫then時丟擲了異常,則:  
  //                 如果 resolve 或 reject 已經被呼叫了,則忽略它。isUsed = true;  
  //                 否則, 以e為reason將 promise 拒絕。   
 //         4.如果 then不是一個函式,則 以x為值resolve promise。 
   //    
  */  
  // } else    
 if (x !== null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是否是thenable物件(具有then方法的物件/函式)  如果在取x.then值時丟擲了異常,則以這個異常做為原因將promise拒絕。
            let then = x.then;
            if (typeof then === 'function') { 
               then.call(x, y => {  // 如果y是promise就繼續遞迴解析promise 
                   if (isUsed) return; 
                   isUsed = true; 
                   resolvePromise(promise2, y, resolve, reject);
                }, reason => {  // 只要失敗了就失敗了 不用再遞迴解析是都是promise   
                 if (isUsed) return; 
                   isUsed = true; 
                   reject(reason); 
               })
            } else { // 說明是一個函式,則 以x為值resolve promise。
                resolve(x); 
           }
        } catch (e) {   
         if (isUsed) return; 
           isUsed = true;
            reject(e);
        }
    } else {  //4、如果 x 不是物件也不是函式,則以x為值 resolve promise。例如 x = 123 或 x ='成功'
        resolve(x);
    }}


// Promise 物件代表了未來將要發生的事件,用來傳遞非同步操作的訊息。
// Promise 物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise 物件提供統一的介面,使得控制非同步操作更加容易。

class Promise {    // Promise 是一個類, new Promise 返回一個 promise物件 接收一個ex執行函式作為引數, ex有兩個函式型別形參resolve reject   
 /**    
 * var promise = new Promise(function(resolve, reject) { //會立即執行        // 非同步處理      
  // 處理結束後、呼叫resolve 或 reject
   //當非同步程式碼執行成功時,我們才會呼叫resolve(...), 當非同步程式碼失敗時就會呼叫reject(...)        
//在本例中,我們使用setTimeout(...)來模擬非同步程式碼,實際編碼時可能是XHR請求或是HTML5的一些API方法.        
   setTimeout(function(){  
    resolve("成功!"); //程式碼正常執行!
        }, 250); 
     });
      promise.then(function(successMessage){
            //successMessage的值是上面呼叫resolve(...)方法傳入的值. 
           //successMessage引數不一定非要是字串型別,這裡只是舉個例子
            console.log("Yay! " + successMessage); 
       });    
      */    

constructor(ex) {  // 

  this.status = 'pending'; // 初始狀態 (表示 未開始)
 this.resolveVal = undefined;  // resolved狀態時(表示成功) 返回的資訊
 this.rejectVal = undefined;  // rejected狀態時(表示失敗) 返回的資訊
 this.onResolveCallBackFns = [];   // 儲存resolved狀態對應的onResolved函式 (因為可以鏈式呼叫可以多個then方法)
        this.onRejectCallBackFns = [];    // 儲存rejected狀態對應的onRejected函式

        /**    
* 一個Promise必須處在其中之一的狀態:pending, fulfilled 或 rejected. 
        *          * 如果是pending狀態,則promise:
            可以轉換到resolved或rejected狀態。
            如果是resolved狀態,則promise:
            不能轉換成任何其它狀態。
            必須有一個值,且這個值不能被改變。
            如果是rejected狀態,則promise可以:
            不能轉換成任何其它狀態。
            必須有一個原因,且這個值不能被改變。
            ”值不能被改變”指的是其identity不能被改變,而不是指其成員內容不能被改變。 
       */
        let resolve = (data) => {   // data 成功態時接收的終值 
           if (this.status === 'pending') { 
 // 只能由pedning狀態 => resolved狀態 避免呼叫多次resolve reject)  
              this.status = 'resolved';
                this.resolveVal = data;
                this.onResolveCallBackFns.forEach(cb => cb()); 
           } 
       }

        let reject = (err) => { 
           if (this.status === 'pending') {
  // 只能由pedning狀態 => rejected狀態 避免呼叫多次resolve reject)
                this.status = 'rejected'; 
               this.rejectVal = err; 
               this.onRejectCallBackFns.forEach(cb => cb());
            } 
       }
        // 捕獲在ex執行器中丟擲的異常
        // new Promise((resolve, reject) => {
        //     throw new Error('error in ex')
        // }) 
       try { 
           ex(resolve, reject); 
       } catch (e) { 
           reject(e);
        }
    }
    //  按照prmoise a+ 規範  then方法接受兩個引數  then方法是非同步執行的  必須返回一個promise    then(resolve, reject) {        //  then 方法 resolve,reject 都是可選引數 保證引數後續能夠繼續執行        //      
 1.1、  如果resolve不是一個函式,則忽略之。 如果reject不是一個函式,則忽略之。 
       resolve = typeof resolve == 'function' ? resolve : y => y;     
       reject = typeof reject == 'function' ? reject : err => { throw err };

        let promise2; // then必須返回一個promise
        if (this.status === 'pending') { // 等待態  
          // 當非同步呼叫resolve/rejected時 將resolve/reject收集暫存到集合中          
            promise2 = new Promise((res, rej) => { 
               this.onResolveCallBackFns.push(() => {
                    setTimeout(() => { 
                       try {   
                         // resolvePromise可以解析x和promise2之間的關係                             /** 如果resolve 或 reject 返回了值x, 則執行Promise 解析流程[[Resolve]](promise2, x).                            // 如果resolve 或 reject 丟擲了異常e, 則promise2應當以e為rejectVal被拒絕。                            // 如果 resolve 不是一個函式且promise1已經resolved,則promise2必須以promise1的值resolved.                            // 如果 reject 不是一個函式且promise1已經rejected, 則promise2必須以相同的rejectVal被拒絕.                            */                            let x = resolve(this.resolveVal);                            resolvePromise(promise2, x, res, rej);                        } catch (e) {                            rej(e);                        }                    }, 0);                                   });

                this.onRejectCallBackFns.push(() => { 
                   setTimeout(() => { 
                       try { 
                           let x = reject(this.rejectVal); 
                           resolvePromise(promise2, x, res, rej); 
                       } catch (e) { 
                           rej(e);  
                      } 
                     }, 0); 
                     });   
         }); 
       }
        if (this.status == 'resolved') { 
  // 它必須在promise resolved後呼叫, 且promise的value為其第一個引數。
            promise2 = new Promise((res, rej) => {
                //  用setTimeout方法原因 : 
                //  1、方法是非同步的
                 // 2、  對於一個promise,它的then方法可以呼叫多次.(當在其他程式中多次呼叫同一個promise的then時 由於之前狀態已經為resolved/rejected狀態,則會走的下面邏輯),
                // 所以要確保為resolved/rejected狀態後 也要非同步執行resolve/reject 保持統一 
               setTimeout(() => { 
                   try { 
                      let x = resolve(this.resolveVal);
                        resolvePromise(promise2, x, res, rej); // 
 // resolvePromise可以解析x和promise2之間的關係 
                    } catch (e) { 
                       rej(e);  
                  }
                })
            })
        }
        if (this.status == 'rejected') {  // 必須在promise rejected後呼叫, 且promise的rejectVal為其第一個引數 
           promise2 = new Promise((res, rej) => {                //    方法是非同步的  所以用setTimeout方法   
              setTimeout(() => {
                    try { 
                       let x = reject(this.rejectVal);  
                      resolvePromise(promise2, x, res, rej);   // resolvePromise可以解析x和promise2之間的關係  
                   } catch (e) { 
                       rej(e); 
                   }
                });
            })
        }
        return promise2; // 呼叫then後返回一個新的promise    }
    // catch接收的引數 只用錯誤 catch就是then的沒有成功的簡寫 
   catch(err) { 
       return this.then(null, err);
    }}

/** * Promise.all Promise進行並行處理 
* 引數: arr物件組成的陣列作為引數
 * 返回值: 返回一個Promise例項
 * 當這個陣列裡的所有promise物件全部變為resolve狀態的時候,才會resolve。 
*/
Promise.all = function (arr) {
    return new Promise((resolve, reject) => {
        let num = 0, innerArr = [];
        function done(index, data) {
            innerArr[index] = data;
            num++; 
           if (num === arr.length) {
                resolve(innerArr); 
           }
        }
        for (let i = 0; i < arr.length; i++) { 
           arr[i].then((res) => {
                done(i, res); 
           }, reject); // 有一個失敗 就返回 
       }    })}

/** * Promise.race 
* 引數: 接收 promise物件組成的陣列作為引數 
* 返回值: 返回一個Promise例項 
* 只要有一個promise物件進入 resolved 或者 rejected 狀態的話,就會繼續進行後面的處理(取決於哪一個更快) 
*/
Promise.race = function (arr) {
    return new Promise((resolve, reject) => { 
       arr.forEach((item, index) => {
            item.then(resolve, reject); 
       }); 
   });}
// Promise.reject 返回一個rejected狀態的promise物件

Promise.resolve = function (val) {  
  return new Promise((resolve, reject) => resolve(val))}

// .Promise.resolve 返回一個fulfilled狀態的promise物件
Promise.reject = function (val) {   
 return new Promise((resolve, reject) => reject(val));}

Promise.deferred = Promise.defer = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject; 
   }) 
   return dfd;}

module.exports = Promise;複製程式碼


6、測試

let p = new Promise((resolve, reject) => { 
 reject('err');})
p.then().then().catch(r => {  
console.log(r);}).
then(data => {  
console.log('data', data);})

執行結果:
errdata undefined
複製程式碼

let fs = require('fs');
function read() { 
 // 好處就是解決巢狀問題  
// 壞處錯誤處理不方便了 
 let defer = Promise.defer(); 
 fs.readFile('./1.txt', 'utf8', (err, data) => { 
   if (err) defer.reject(err); 
   defer.resolve(data)  }); 
 return defer.promise;}
read().then(data => {  
console.log(data);});

執行結果
我是1.txt內容複製程式碼


相關文章