你真的會用 Promise 嗎?

前言:回調地獄

試想一下,有 3 個異步請求,第二個須要依賴第一個請求的返回結果,第三個須要依賴第二個請求的返回結果,通常怎麼作?javascript

try{
  // 請求1
  $.ajax({
    url: url1,
    success: function(data1){
      // 請求2
      try{
        $.ajax({
          url: url1,
          data: data1,
          success: function(data2){
            try{
              // 請求3
              $.ajax({
                url: url1,
                data: data2,
                success: function(data3){
                  // 後續業務邏輯...
                }
              });
            }catch(ex3){
              // 請求3的異常處理
            }
          }
        })
      }catch(ex){
        // 請求2的異常處理
      }
    }
  })
}catch(ex1){
  // 請求1的異常處理
}
複製代碼

顯然,若是再加上覆雜的業務邏輯、異常處理,代碼會更臃腫。在一個團隊中,對這種代碼的 review 和維護將會很痛苦。前端

回調地獄帶來的負面做用有如下幾點:java

  • 代碼臃腫。
  • 可讀性、可維護性差。
  • 耦合度高、可複用性差。
  • 容易滋生 bug。
  • 異常處理很噁心,只能在回調裏處理異。

Promise 全解

什麼是 Promise?

  • Promise 是一種異步編程解決方案,避免回調地獄,能夠把異步代碼寫得像同步同樣。
  • Promise 是一個對象,用於表示一個異步操做的最終狀態(完成或失敗),以及該異步操做的結果值。
  • Promise 是一個代理(代理一個值),被代理的值在Promise對象建立時多是未知的。它容許你爲異步操做的成功和失敗分別綁定相應的處理方法(handlers)。 這讓異步方法能夠像同步方法那樣返回值,但並非當即返回最終執行結果,而是一個能表明將來出現的結果的 promise 對象。
var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('foo');
  }, 300);
});

promise1.then(function(value) {
  console.log(value);
  // after 300ms, expected output: "foo"
});
複製代碼

Promise 核心特性?

  1. 一個 Promise 有 3 種狀態:git

    • pending: 初始狀態,既不是成功,也不是失敗狀態。
    • fulfilled: 意味着操做成功完成。
    • rejected: 意味着操做失敗。

    pending 狀態的 Promise 可能會變爲fulfilled 狀態,也可能變爲 rejected 狀態。github

  1. Promise 對象的狀態,只有內部可以改變(並且只能改變一次),不受外界影響。面試

  2. 對象的狀態一旦改變,就不會再變,任什麼時候候均可以獲得這個結果。 Promise 對象的狀態改變,只有兩種可能:從 Pending 變爲 Resolved 和從 Pending 變爲 Rejected。一旦狀態發生改變,狀態就凝固了,會一直保持這個結果。ajax

const p = new Promise((resolve, reject)=>{
  resolve("resolved first time!"); // 只有第一次有效
  resolve("resolved second time!");
  reject("rejected!");
});
p.then(
  (data)=>console.log(data), 
  (error)=>console.log(error)
);
複製代碼

Promise API

// 1. 構造方法
const p = new Promise((resolve, reject) => { /* executor*/
    // 1.1. Promise構造函數執行時當即調用 executor 函數;
    // 1.2. resolve 和 reject 函數被調用時,分別將promise的狀態改成fulfilled(完成)或rejected(失敗)
    // 1.3. 若是在executor函數中拋出一個錯誤,那麼該promise 狀態爲rejected。
    // 1.4. executor函數的返回值被忽略。
});

// 2.原型方法
Promise.prototype.catch(onRejected)
Promise.prototype.then(onFulfilled, onRejected)

// 3.靜態方法
Promise.all(iterable);
Promise.race(iterable);
Promise.reject(reason);
Promise.resolve(value);
複製代碼

示例:用 Promise 和 XMLHttpRequest 加載圖像

function imgLoad(url) {
    // Create new promise with the Promise() constructor;
    // This has as its argument a function
    // with two parameters, resolve and reject
    return new Promise(function(resolve, reject) {
      // Standard XHR to load an image
      var request = new XMLHttpRequest();
      request.open('GET', url);
      request.responseType = 'blob';
      // When the request loads, check whether it was successful
      request.onload = function() {
        if (request.status === 200) {
          // If successful, resolve the promise by passing back the request response
          resolve(request.response);
        } else {
          // If it fails, reject the promise with a error message
          reject(Error('Image didn\'t load successfully; error code:' + request.statusText));
        }
      };
      request.onerror = function() {
          // Also deal with the case when the entire request fails to begin with
          // This is probably a network error, so reject the promise with an appropriate message
          reject(Error('There was a network error.'));
      };
      // Send the request
      request.send();
    });
  }
  // Get a reference to the body element, and create a new image object
  var body = document.querySelector('body');
  var myImage = new Image();
  // Call the function with the URL we want to load, but then chain the
  // promise then() method on to the end of it. This contains two callbacks
  imgLoad('myLittleVader.jpg').then(function(response) {
    // The first runs when the promise resolves, with the request.response
    // specified within the resolve() method.
    var imageURL = window.URL.createObjectURL(response);
    myImage.src = imageURL;
    body.appendChild(myImage);
    // The second runs when the promise
    // is rejected, and logs the Error specified with the reject() method.
  }, function(Error) {
    console.log(Error);
  });
複製代碼

Promise 與事件循環機制

Event Loop 中的事件,分爲 MacroTask(宏任務)和 MicroTask(微任務)。npm

  • MacroTask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
  • MicroTask: process.nextTick, Promises, Object.observe, MutationObserver

通俗來講,MacroTasks 和 MicroTasks 最大的區別在它們會被放置在不一樣的任務調度隊列中。 編程

每一次事件循環中,主進程都會先執行一個MacroTask 任務,這個任務就來自於所謂的MacroTask Queue隊列;當該 MacroTask 執行完後,Event loop 會立馬調用 MicroTask 隊列的任務,直到消費完全部的 MicroTask,再繼續下一個事件循環。

注:async/await 本質上仍是基於Promise的一些封裝,而Promise是屬於微任務的一種。因此在使用 await 關鍵字與 Promise.then 效果相似。即:async 函數在 await 以前的代碼都是同步執行的,能夠理解爲await以前的代碼屬於new Promise時傳入的代碼,await以後的全部代碼都是在Promise.then中的回調;數組

Promise 常見面試題目

題目:寫出運行結果

setTimeout(function(){
    console.log(1);
}, 0)
new Promise(function(resolve){
    console.log(2);
    resolve();
    console.log(3);
}).then(function(){
    console.log(4);
})
console.log(5);
複製代碼

答案 & 解析:

// 解析:
// 1. new Promise(fn)後,函數fn會當即執行;
// 2. fn在執行過程當中,因爲調用了resolve,使得Promise當即轉換爲resolve狀態,
// 這也促使p.then(fn)中的函數fn被當即放入microTask隊列中,所以fn將會在
// 本輪事件循環的結束時執行,而不是下一輪事件循環的開始時執行;
// 3. setTimeout屬於macroTask,是在下一輪事件循環中執行;
//答案:
// 2 3 5 4 1
複製代碼

題目:寫出運行結果

Promise.resolve(1)
  .then((res) => {
    console.log(res);
    return 2;
  })
  .catch((res) => {
    console.log(res);
    return 3;
  })
  .then((res) => {
    console.log(res);
  });
複製代碼

答案 & 解析:

// 解析:每次調用p.then或p.catch都會返回一個新的promise,
//       從而實現了鏈式調用;第一個.then中未拋出異常,
//       因此不會被.catch語句捕獲,會正常進入第二個.then執行;
// 答案:1 2
複製代碼

題目:寫出運行結果

Promise.resolve()
  .then( () => {
    return new Error('error!')
  })
  .then( res => {
    console.log('then: ', res)
  })
  .catch( err => {
    console.log('catch: ', err)
  });
複製代碼

答案 & 解析:

// 解析:在 .then 或 .catch 中 return 一個 error 對象並不會拋出錯誤,
//       因此不會被後續的 .catch 捕獲;
// 答案:then:  Error: error!
//          at ...
//          at ...
複製代碼

題目:寫出運行結果

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log);
複製代碼

答案 & 解析:

// 解析:p.then、.catch 的入參應該是函數,傳入非函數則會發生值穿透;
// 答案:1
複製代碼

題目:寫出運行結果

Promise.resolve()
  .then(
    value => { throw new Error('error'); }, 
    reason => { console.error('fail1:', reason); }
  )
  .catch(
    reason => { console.error('fail2:', reason); }
  );
複製代碼

答案 & 解析:

// 解析:.then能夠接收兩個參數:.then(onResolved, onRejected)
//       .catch是.then的語法糖:.then(onRejected) ==> .then(null, onRejected)
// 答案:fail2: Error: error
//       at .....
//       at .....
複製代碼

題目:寫出運行結果

console.log(1);
new Promise(function (resolve, reject){
    reject();
    resolve();
}).then(function(){
    console.log(2);
}, function(){
    console.log(3);
});
console.log(4);
複製代碼

答案 & 解析:

// 解析:Promise狀態的一旦變成resolved或rejected,
//       Promise的狀態和值就固定下來了,
//       不論你後續再怎麼調用resolve或reject方法,
//       都不能改變它的狀態和值。
// 
// 答案:1 4 3
複製代碼

題目:寫出運行結果

new Promise(resolve => { // p1
    resolve(1);
    
    // p2
    Promise.resolve().then(() => {
      console.log(2); // t1
    });

    console.log(4)
}).then(t => {
  console.log(t); // t2
});

console.log(3);
複製代碼

答案 & 解析:

// 解析:
// 1. new Promise(fn), fn 當即執行,因此先輸出 4;
// 2. p1和p2的Promise在執行then以前都已處於resolve狀態,
//    故按照then執行的前後順序,將t一、t2放入microTask中等待執行;
// 3. 完成執行console.log(3)後,macroTask執行結束,而後microTask
//    中的任務t一、t2依次執行,因此輸出三、二、1;
// 答案:
// 4 3 2 1
複製代碼

題目:寫出運行結果

Promise.reject('a')
  .then(()=>{  
    console.log('a passed'); 
  })
  .catch(()=>{  
    console.log('a failed'); 
  });  
Promise
  .reject('b')
  .catch(()=>{  
    console.log('b failed'); 
  })
  .then(()=>{  
    console.log('b passed');
  })
複製代碼

答案 & 解析:

// 解析:p.then(fn)、p.catch(fn)中的fn都是異步執行,上述代碼可理解爲:
//       setTimeout(function(){
//             setTimeout(function(){
//                  console.log('a failed'); 
//             });  
//       });
//       setTimeout(function(){
//             console.log('b failed');
//
//             setTimeout(function(){
//                  console.log('b passed'); 
//             });
//       });
// 答案:b failed
//       a failed
//       b passed
複製代碼

題目:寫出運行結果

async function async1() {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
}

async function async2() {
   console.log('async2')
}

console.log('script start');

setTimeout(function () {
   console.log('settimeout')
})

async1();

new Promise(function (resolve) {
   console.log('promise1');
   resolve();
}).then(function () {
   console.log('promise2');
})

console.log('script end');
複製代碼

答案:(不解析了,你們研究一下)

script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout
複製代碼

本身實現一版 Promise

Promise有不少社區規範,如 Promise/A、Promise/B、Promise/D 以及 Promise/A 的升級版 Promise/A+;Promise/A+ 是 ES6 Promises 的前身,並且網絡上有不少可供學習、參考的開源實現(例如:Adehun、bluebird、Q、ypromise等)。

Promise 的規範去哪找?

Promise/A+ 規範:
github.com/promises-ap…

如何保證本身實現的 Promise 符合規範?

用官方的Promise規範測試集,測試本身的實現。

Promise/A+ 規範測試集:
github.com/promises-ap…

開始編碼

識別核心接口

能夠看出,共需實現7個接口;
複製代碼

分析接口間聯繫

能夠看出,7個接口中,只有構造函數RookiePromise和成員函數then算核心接口,其餘接口都可經過這兩個接口實現;
複製代碼

仔細閱讀官方規範,逐條合規編碼

構建主框架

編寫狀態轉換邏輯

Promise 對象的狀態改變,只有兩種可能:pending -> fulfilled 和 pending -> rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果;
——《ES6 標準入門(第三版)》

注:_notify函數用做異步執行傳入的函數數組以及參數;代碼中將_callbacks、_errbacks傳給_notify函數後當即清空,是爲了保證_callbacks、_errbacks至多被執行一次;

實現 then 接口

實現resolve(promise, x)接口

完整 RookiePromise 源碼實現

/** * 2.1. Promise States * A promise must be in one of three states: * pending, fulfilled, or rejected. */
const STATE_PENDING = "pending";
const STATE_FULFILLED = "fulfilled";
const STATE_REJECTED = "rejected";

function RookiePromise(fn) {
  this._state = STATE_PENDING;
  this._value = undefined;
  this._callbacks = [];
  this._errorbacks = [];

  /** * 2.3. The Promise Resolution Procedure * The promise resolution procedure is an abstract operation * taking as input a promise and a value, which we denote as * [[Resolve]](promise, x) */
  var executed = false; // 用於保證resolve接口只有第一次被觸發時有效;
  function resolve(promise, x){
    if(executed){
      return;
    }
    executed = true;

    var innerResolve = (promise, x) => {
      if(promise === x){
        // 2.3.1. If promise and x refer to the same object, 
        // reject promise with a TypeError as the reason.
        this._reject(new TypeError("出錯了, promise === x, 會形成死循環!"));
      }else if(x instanceof RookiePromise){
        // 2.3.2. If x is a promise, adopt its state [3.4]:
        // 2.3.2.1. If x is pending, promise must remain pending until x is fulfilled or rejected.
        // 2.3.2.2. If/when x is fulfilled, fulfill promise with the same value.
        // 2.3.2.3. If/when x is rejected, reject promise with the same reason.
        if(x._state == STATE_PENDING){
          x.then((value) => {
            innerResolve(promise, value);
          }, (reason) => {
            this._reject(reason);
          });
        }else if(x._state == STATE_FULFILLED){
          this._fulfill(x._value);
        }else if(x._state == STATE_REJECTED){
          this._reject(x._value);
        }
      }else if(x && (typeof x == "function" || typeof x == "object")){
        // 2.3.3. Otherwise, if x is an object or function,
        try{
          // 2.3.3.1. Let then be x.then.
          let then = x.then;

          if(typeof then === "function"){ //thenable
            var executed = false;
            try{
              // 2.3.3.3. If then is a function, call it with x as this, 
              // first argument resolvePromise, and 
              // second argument rejectPromise, 
              // where:
              then.call(x, (value) => {
                // 2.3.3.3.3. If both resolvePromise and rejectPromise are called, 
                // or multiple calls to the same argument are made, 
                // the first call takes precedence, and any further calls are ignored.
                if(executed){
                  return;
                }  
                executed = true;
                // 2.3.3.3.1. If/when resolvePromise is called with a value y, 
                // run [[Resolve]](promise, y).
                innerResolve(promise, value);
              }, (reason) => {
                // 2.3.3.3.3. If both resolvePromise and rejectPromise are called, 
                // or multiple calls to the same argument are made, 
                // the first call takes precedence, and any further calls are ignored.
                if(executed){
                  return;
                }  
                executed = true;
                // 2.3.3.3.2. If/when rejectPromise is called with a reason r, 
                // reject promise with r.
                this._reject(reason);
              });
            }catch(e){
              // 2.3.3.3.4. If calling then throws an exception e,
              // 2.3.3.3.4.1. If resolvePromise or rejectPromise have been called, ignore it.
              if(executed){
                return;
              }
              // 2.3.3.3.4.2. Otherwise, reject promise with e as the reason.
              throw e;
            }
          }else{
            // 2.3.3.4. If then is not a function, fulfill promise with x.
            this._fulfill(x);
          }
        }catch(ex){
          // 2.3.3.2. If retrieving the property x.then results in a thrown exception e, 
          // reject promise with e as the reason.
          this._reject(ex);
        }
      }else{
        // 2.3.4. If x is not an object or function, fulfill promise with x.
        this._fulfill(x);
      }
    };
    innerResolve(promise, x)
  }
  
  function reject(promise, reason){
    this._reject(reason);
  }

  resolve = resolve.bind(this, this); // 經過bind模擬規範中的 [[Resolve]](promise, x) 行爲
  reject = reject.bind(this, this);

  fn(resolve, reject); // new RookiePromise((resolve, reject) => { ... })
}

/** * 2.1. Promise States * * A promise must be in one of three states: pending, fulfilled, or rejected. * * 2.1.1. When pending, a promise: * 2.1.1.1 may transition to either the fulfilled or rejected state. * 2.1.2. When fulfilled, a promise: * 2.1.2.1 must not transition to any other state. * 2.1.2.2 must have a value, which must not change. * 2.1.3. When rejected, a promise: * 2.1.3.1 must not transition to any other state. * 2.1.3.2 must have a reason, which must not change. * * Here, 「must not change」 means immutable identity (i.e. ===), * but does not imply deep immutability. */
RookiePromise.prototype._fulfill = function(value) {
  if(this._state == STATE_PENDING){
    this._state = STATE_FULFILLED;
    this._value = value;

    this._notify(this._callbacks, this._value);

    this._errorbacks = [];
    this._callbacks = [];
  }
}
RookiePromise.prototype._reject = function(reason) {
  if(this._state == STATE_PENDING){
    this._state = STATE_REJECTED;
    this._value = reason;

    this._notify(this._errorbacks, this._value);

    this._errorbacks = [];
    this._callbacks = [];
  }
}
RookiePromise.prototype._notify = function(fns, param) {
  setTimeout(()=>{
    for(var i=0; i<fns.length; i++){
      fns[i](param);
    }
  }, 0);
}

/** * 2.2. The then Method * A promise’s then method accepts two arguments: * promise.then(onFulfilled, onRejected) */
RookiePromise.prototype.then = function(onFulFilled, onRejected) {
  // 2.2.7. then must return a promise [3.3].
  // promise2 = promise1.then(onFulFilled, onRejected);
  //
  return new RookiePromise((resolve, reject)=>{
    // 2.2.1. Both onFulfilled and onRejected are optional arguments:
    // 2.2.1.1. If onFulfilled is not a function, it must be ignored.
    // 2.2.1.2. If onRejected is not a function, it must be ignored.
    if(typeof onFulFilled == "function"){
      this._callbacks.push(function(value){
        try{
          // 2.2.5. onFulfilled and onRejected must be called as functions (i.e. with no this value)
          var value = onFulFilled(value);
          resolve(value);
        }catch(ex){
          // 2.2.7.2. If either onFulfilled or onRejected throws an exception e, 
          // promise2 must be rejected with e as the reason.
          reject(ex);
        }
      });
    }else{
      // 2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, 
      // promise2 must be fulfilled with the same value as promise1.
      this._callbacks.push(resolve); // 值穿透
    }

    if(typeof onRejected == "function"){
      this._errorbacks.push(function(reason){
        try{
          // 2.2.5. onFulfilled and onRejected must be called as functions (i.e. with no this value)
          var value = onRejected(reason);
          resolve(value);
        }catch(ex){
          // 2.2.7.2. If either onFulfilled or onRejected throws an exception e, 
          // promise2 must be rejected with e as the reason.
          reject(ex);
        }
      });
    }else{
      // 2.2.7.4. If onRejected is not a function and promise1 is rejected, 
      // promise2 must be rejected with the same reason as promise1.
      this._errorbacks.push(reject); // 值穿透
    }

    // 2.2.6. then may be called multiple times on the same promise.
    // 2.2.6.1. If/when promise is fulfilled, all respective onFulfilled callbacks must 
    // execute in the order of their originating calls to then.
    // 2.2.6.2. If/when promise is rejected, all respective onRejected callbacks must 
    // execute in the order of their originating calls to then.
    if(this._state == STATE_REJECTED){
      // 2.2.4. onFulfilled or onRejected must not be called until the 
      // execution context stack contains only platform code.
      this._notify(this._errorbacks, this._value);
      this._errorbacks = [];
      this._callbacks = [];
    }else if(this._state == STATE_FULFILLED){
      // 2.2.4. onFulfilled or onRejected must not be called until the 
      // execution context stack contains only platform code.
      this._notify(this._callbacks, this._value);
      this._errorbacks = [];
      this._callbacks = [];
    }
  });
};

RookiePromise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
};
RookiePromise.resolve = function(value) {
    return new RookiePromise((resolve, reject) => resolve(value));
};
RookiePromise.reject = function(reason) {
    return new RookiePromise((resolve, reject) => reject(reason));
};
RookiePromise.all = function(values) {
    return new Promise((resolve, reject) => {
      var result = [], remaining = values.length;
      function resolveOne(index){
        return function(value){
          result[index] = value;
          remaining--;
          if(!remaining){
            resolve(result);
          }
        };
      }
        for (var i = 0; i < values.length; i++) {
            RookiePromise.resolve(values[i]).then(resolveOne(i), reject);
        }
    });
};
RookiePromise.race = function(values) {
    return new Promise((resolve, reject) => {
        for (var i = 0; i < values.length; i++) {
            RookiePromise.resolve(values[i]).then(resolve, reject);
        }
    });
};

module.exports = RookiePromise;
複製代碼

RookiePromise 編碼小結

RookiePromise的結構是按照Promise/A+規範中對then、resolve接口的描述組織的;優勢是編碼過程直觀,缺點是innerResolve函數篇幅太長、頭重腳輕,不夠和諧;相信各位能夠寫出更漂亮的版本;

測試正確性

安裝 Promise/A+測試工具

npm install –save promises-aplus-tests

編寫 RookiePromise 的測試適配器

RookiePromise須要額外提供3個靜態接口,供Promise/A+自動測試工具調用;

/** * In order to test your promise library, * you must expose a very minimal adapter interface. * These are written as Node.js modules with a few well-known exports: * * resolved(value): creates a promise that is resolved with value. * rejected(reason): creates a promise that is already rejected with reason. * deferred(): creates an object consisting of { promise, resolve, reject }: * promise is a promise that is currently in the pending state. * resolve(value) resolves the promise with value. * reject(reason) moves the promise from the pending state to the rejected state, * with rejection reason reason. * * https://github.com/promises-aplus/promises-tests */
var RookiePromise = require('./RookiePromise.js');

RookiePromise.resolved = RookiePromise.resolve;
RookiePromise.rejected = RookiePromise.reject;
RookiePromise.deferred = function() {
    let defer = {};
    defer.promise = new RookiePromise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}
module.exports = RookiePromise
複製代碼

執行測試

npx promises-aplus-testsRookiePromiseTestAdapter.js > log.txt

完美經過測試,RookiePromise 是符合 Promise/A+規範的!!!

參考:

《ES6 標準入門(第三版)》
《深刻理解ES6》
MDN(Promise):
developer.mozilla.org/en-US/docs/…
Promise 示例(Promise 和 XMLHttpRequest 加載圖像):
github.com/mdn/js-exam…
States and Fates:
github.com/domenic/pro…
Promise/A+規範文檔:
github.com/promises-ap…
Promise/A+規範測試集:
github.com/promises-ap…
符合Promise/A+規範的一些開源實現:
github.com/promises-ap…

社區以及公衆號發佈的文章,100%保證是咱們的原創文章,若是有錯誤,歡迎你們指正。

文章首發在WebJ2EE公衆號上,歡迎你們關注一波,讓咱們你們一塊兒學前端~~~

再來一波號外,咱們成立WebJ2EE公衆號前端吹水羣,你們不論是看文章仍是在工做中前端方面有任何問題,咱們均可以在羣內互相探討,但願可以用咱們的經驗幫更多的小夥伴解決工做和學習上的困惑,歡迎加入。

相關文章
相關標籤/搜索