Promise知識彙總和麪試狀況

寫在前面

Javascript異步編程前後經歷了四個階段,分別是Callback階段,Promise階段,Generator階段和Async/Await階段。Callback很快就被發現存在回調地獄和控制權問題,Promise就是在這個時間出現,用以解決這些問題,Promise並不是一個新事務,而是按照一個規範實現的類,這個規範有不少,如 Promise/APromise/BPromise/D 以及 Promise/A 的升級版 Promise/A+,最終 ES6 中採用了 Promise/A+ 規範。後來出現的Generator函數以及Async函數也是以Promise爲基礎的進一步封裝,可見Promise在異步編程中的重要性。 前端

關於Promise的資料已經不少,但每一個人理解都不同,不一樣的思路也會有不同的收穫。這篇文章會着重寫一下Promise的實現以及筆者在平常使用過程當中的一些心得體會。node

實現Promise

規範解讀

Promise/A+規範主要分爲術語、要求和注意事項三個部分,咱們重點看一下第二部分也就是要求部分,以筆者的理解大概說明一下,具體細節參照完整版Promise/A+標準。git

一、Promise有三種狀態pendingfulfilledrejected。(爲了一致性,此文章稱fulfilled狀態爲resolved狀態)github

  • 狀態轉換隻能是pendingresolved或者pendingrejected
  • 狀態一旦轉換完成,不能再次轉換。

二、Promise擁有一個then方法,用以處理resolvedrejected狀態下的值。面試

  • then方法接收兩個參數onFulfilledonRejected,這兩個參數變量類型是函數,若是不是函數將會被忽略,而且這兩個參數都是可選的。
  • then方法必須返回一個新的promise,記做promise2,這也就保證了then方法能夠在同一個promise上屢次調用。(ps:規範只要求返回promise,並無明確要求返回一個新的promise,這裏爲了跟ES6實現保持一致,咱們也返回一個新promise
  • onResolved/onRejected有返回值則把返回值定義爲x,並執行[[Resolve]](promise2, x);
  • onResolved/onRejected運行出錯,則把promise2設置爲rejected狀態;
  • onResolved/onRejected不是函數,則須要把promise1的狀態傳遞下去。

三、不一樣的promise實現能夠的交互。編程

  • 規範中稱這一步操做爲promise解決過程,函數標示爲[[Resolve]](promise, x),promise爲要返回的新promise對象,xonResolved/onRejected的返回值。若是xthen方法且看上去像一個promise,咱們就把x當成一個promise的對象,即thenable對象,這種狀況下嘗試讓promise接收x的狀態。若是x不是thenable對象,就用x的值來執行 promise
  • [[Resolve]](promise, x)函數具體運行規則:segmentfault

    • 若是 promisex 指向同一對象,以 TypeError 爲據因拒絕執行 promise;
    • 若是 xPromise ,則使 promise 接受 x 的狀態;
    • 若是 x 爲對象或者函數,取x.then的值,若是取值時出現錯誤,則讓promise進入rejected狀態,若是then不是函數,說明x不是thenable對象,直接以x的值resolve,若是then存在而且爲函數,則把x做爲then函數的做用域this調用,then方法接收兩個參數,resolvePromiserejectPromise,若是resolvePromise被執行,則以resolvePromise的參數value做爲x繼續調用[[Resolve]](promise, value),直到x不是對象或者函數,若是rejectPromise被執行則讓promise進入rejected狀態;
    • 若是 x 不是對象或者函數,直接就用x的值來執行promise

代碼實現

規範解讀第1條,代碼實現:數組

class Promise {
  // 定義Promise狀態,初始值爲pending
  status = 'pending';
  // 狀態轉換時攜帶的值,由於在then方法中須要處理Promise成功或失敗時的值,因此須要一個全局變量存儲這個值
  data = '';

  // Promise構造函數,傳入參數爲一個可執行的函數
  constructor(executor) {
    // resolve函數負責把狀態轉換爲resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
    }
    // reject函數負責把狀態轉換爲rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
    }

    // 直接執行executor函數,參數爲處理函數resolve, reject。由於executor執行過程有可能會出錯,錯誤狀況須要執行reject
    try {
      executor(resolve, reject);
    } catch(e) {
      reject(e)
    }
  }
}

規範解讀第2條,代碼實現:promise

/**
    * 擁有一個then方法
    * then方法提供:狀態爲resolved時的回調函數onResolved,狀態爲rejected時的回調函數onRejected
    * 返回一個新的Promise
  */
  then(onResolved, onRejected) {
    // 設置then的默認參數,默認參數實現Promise的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };
    
    let promise2;
    
    promise2 =  new Promise((resolve, reject) => {
      // 若是狀態爲resolved,則執行onResolved
      if (this.status === 'resolved') {
        try {
          // onResolved/onRejected有返回值則把返回值定義爲x
          const x = onResolved(this.data);
          // 執行[[Resolve]](promise2, x)
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }
      // 若是狀態爲rejected,則執行onRejected
      if (this.status === 'rejected') {
        try {
          const x = onRejected(this.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }
    });
    
    return promise2;
  }

如今咱們就按照規範解讀第2條,實現了上述代碼,上述代碼很明顯是有問題的,問題以下瀏覽器

  1. resolvePromise未定義;
  2. then方法執行的時候,promise可能仍然處於pending狀態,由於executor中可能存在異步操做(實際狀況大部分爲異步操做),這樣就致使onResolved/onRejected失去了執行時機;
  3. onResolved/onRejected這兩相函數須要異步調用(官方Promise實現的回調函數老是異步調用的)。

解決辦法:

  1. 根據規範解讀第3條,定義並實現resolvePromise函數;
  2. then方法執行時若是promise仍然處於pending狀態,則把處理函數進行儲存,等resolve/reject函數真正執行的的時候再調用。
  3. promise.then屬於微任務,這裏咱們爲了方便,用宏任務setTiemout來代替實現異步,具體細節特別推薦這篇文章

好了,有了解決辦法,咱們就把代碼進一步完善:

class Promise {
  // 定義Promise狀態變量,初始值爲pending
  status = 'pending';
  // 由於在then方法中須要處理Promise成功或失敗時的值,因此須要一個全局變量存儲這個值
  data = '';
  // Promise resolve時的回調函數集
  onResolvedCallback = [];
  // Promise reject時的回調函數集
  onRejectedCallback = [];

  // Promise構造函數,傳入參數爲一個可執行的函數
  constructor(executor) {
    // resolve函數負責把狀態轉換爲resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
      for (const func of this.onResolvedCallback) {
        func(this.data);
      }
    }
    // reject函數負責把狀態轉換爲rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
      for (const func of this.onRejectedCallback) {
        func(this.data);
      }
    }

    // 直接執行executor函數,參數爲處理函數resolve, reject。由於executor執行過程有可能會出錯,錯誤狀況須要執行reject
    try {
      executor(resolve, reject);
    } catch(e) {
      reject(e)
    }
  }
  /**
    * 擁有一個then方法
    * then方法提供:狀態爲resolved時的回調函數onResolved,狀態爲rejected時的回調函數onRejected
    * 返回一個新的Promise
  */
  then(onResolved, onRejected) {

    // 設置then的默認參數,默認參數實現Promise的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };

    let promise2;

    promise2 =  new Promise((resolve, reject) => {
      // 若是狀態爲resolved,則執行onResolved
      if (this.status === 'resolved') {
        setTimeout(() => {
          try {
            // onResolved/onRejected有返回值則把返回值定義爲x
            const x = onResolved(this.data);
            // 執行[[Resolve]](promise2, x)
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }
      // 若是狀態爲rejected,則執行onRejected
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.data);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }
      // 若是狀態爲pending,則把處理函數進行存儲
      if (this.status = 'pending') {
        this.onResolvedCallback.push(() => {
          setTimeout(() => {
            try {
              const x = onResolved(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });

        this.onRejectedCallback.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }

    });

    return promise2;
  }

  // [[Resolve]](promise2, x)函數
  resolvePromise(promise2, x, resolve, reject) {
    
  }
  
}

至此,規範中關於then的部分就所有實現完畢了。

規範解讀第3條,代碼實現:

// [[Resolve]](promise2, x)函數
  resolvePromise(promise2, x, resolve, reject) {
    let called = false;

    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise!'))
    }
    
    // 若是x仍然爲Promise的狀況
    if (x instanceof Promise) {
      // 若是x的狀態尚未肯定,那麼它是有可能被一個thenable決定最終狀態和值,因此須要繼續調用resolvePromise
      if (x.status === 'pending') {
        x.then(function(value) {
          resolvePromise(promise2, value, resolve, reject)
        }, reject)
      } else { 
        // 若是x狀態已經肯定了,直接取它的狀態
        x.then(resolve, reject)
      }
      return
    }
  
    if (x !== null && (Object.prototype.toString(x) === '[object Object]' || Object.prototype.toString(x) === '[object Function]')) {
      try {
        // 由於x.then有多是一個getter,這種狀況下屢次讀取就有可能產生反作用,因此經過變量called進行控制
        const then = x.then 
        // then是函數,那就說明x是thenable,繼續執行resolvePromise函數,直到x爲普通值
        if (typeof then === 'function') { 
          then.call(x, (y) => { 
            if (called) return;
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          }, (r) => {
            if (called) return;
            called = true;
            reject(r);
          })
        } else { // 若是then不是函數,那就說明x不是thenable,直接resolve x
          if (called) return ;
          called = true;
          resolve(x);
        }
      } catch (e) {
        if (called) return;
        called = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }

這一步驟很是簡單,只要按照規範轉換成代碼便可。

最後,完整的Promise按照規範就實現完畢了,是的,規範裏並無規定catchPromise.resolvePromise.rejectPromise.all等方法,接下來,咱們就看一看Promise的這些經常使用方法。

Promise其餘方法實現

一、catch方法

catch方法是對then方法的封裝,只用於接收reject(reason)中的錯誤信息。由於在then方法中onRejected參數是可不傳的,不傳的狀況下,錯誤信息會依次日後傳遞,直到有onRejected函數接收爲止,所以在寫promise鏈式調用的時候,then方法不傳onRejected函數,只須要在最末尾加一個catch()就能夠了,這樣在該鏈條中的promise發生的錯誤都會被最後的catch捕獲到。

catch(onRejected) {
    return this.then(null, onRejected);
  }
二、done方法

catchpromise鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,可是catch方法內部也可能出現錯誤,因此有些promise實現中增長了一個方法donedone至關於提供了一個不會出錯的catch方法,而且再也不返回一個promise,通常用來結束一個promise鏈。

done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }
三、finally方法

finally方法用於不管是resolve仍是rejectfinally的參數函數都會被執行。

finally(fn) {
    return this.then(value => {
      fn();
      return value;
    }, reason => {
      fn();
      throw reason;
    });
  };
四、Promise.all方法

Promise.all方法接收一個promise數組,返回一個新promise2,併發執行數組中的所有promise,全部promise狀態都爲resolved時,promise2狀態爲resolved並返回所有promise結果,結果順序和promise數組順序一致。若是有一個promiserejected狀態,則整個promise2進入rejected狀態。

static all(promiseList) {
    return new Promise((resolve, reject) => {
      const result = [];
      let i = 0;
      for (const p of promiseList) {
        p.then(value => {
          result[i] = value;
          if (result.length === promiseList.length) {
            resolve(result);
          }
        }, reject);
        i++;
      }
    });
  }
五、Promise.race方法

Promise.race方法接收一個promise數組, 返回一個新promise2,順序執行數組中的promise,有一個promise狀態肯定,promise2狀態即肯定,而且同這個promise的狀態一致。

static race(promiseList) {
    return new Promise((resolve, reject) => {
      for (const p of promiseList) {
        p.then((value) => {
          resolve(value);   
        }, reject);
      }
    });
  }
六、Promise.resolve方法/Promise.reject

Promise.resolve用來生成一個rejected完成態的promisePromise.reject用來生成一個rejected失敗態的promise

static resolve(value) {
    let promise;

    promise = new Promise((resolve, reject) => {
      this.resolvePromise(promise, value, resolve, reject);
    });
  
    return promise;
  }
  
  static reject(reason) {
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  }

經常使用的方法基本就這些,Promise還有不少擴展方法,這裏就不一一展現,基本上都是對then方法的進一步封裝,只要你的then方法沒有問題,其餘方法就均可以依賴then方法實現。

Promise面試相關

面試相關問題,筆者只說一下我司這幾年的狀況,並不能表明所有狀況,參考便可。
Promise是我司前端開發職位,nodejs開發職位,全棧開發職位,必問的一個知識點,主要問題會分佈在Promise介紹、基礎使用方法以及深層次的理解三個方面,問題通常在3-5個,根據面試者回答狀況會適當增減。

一、簡單介紹下Promise。

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。有了Promise對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易。
(固然了也能夠簡單介紹promise狀態,有什麼方法,callback存在什麼問題等等,這個問題是比較開放的)

  • 提問機率:99%
  • 評分標準:人性化判斷便可,此問題通常做爲引入問題。
  • 加分項:熟練說出Promise具體解決了那些問題,存在什麼缺點,應用方向等等。
二、實現一個簡單的,支持異步鏈式調用的Promise類。

這個答案不是固定的,能夠參考最簡實現 Promise,支持異步鏈式調用

  • 提問機率:50%(手擼代碼題,由於這類題目比較耗費時間,一場面試並不會出現不少,因此出現頻率不是很高,但倒是必備知識)
  • 加分項:基本功能實現的基礎上有onResolved/onRejected函數異步調用,錯誤捕獲合理等亮點。
三、Promise.then在Event Loop中的執行順序。(能夠直接問,也能夠出具體題目讓面試者回答打印順序)

JS中分爲兩種任務類型:macrotaskmicrotask,其中macrotask包含:主代碼塊,setTimeoutsetIntervalsetImmediate等(setImmediate規定:在下一次Event Loop(宏任務)時觸發);microtask包含:Promiseprocess.nextTick等(在node環境下,process.nextTick的優先級高於Promise
Event Loop中執行一個macrotask任務(棧中沒有就從事件隊列中獲取)執行過程當中若是遇到microtask任務,就將它添加到微任務的任務隊列中,macrotask任務執行完畢後,當即執行當前微任務隊列中的全部microtask任務(依次執行),而後開始下一個macrotask任務(從事件隊列中獲取)
瀏覽器運行機制可參考這篇文章

  • 提問機率:75%(能夠理解爲4次面試中3次會問到,順即可以考察面試者對JS運行機制的理解)
  • 加分項:擴展講述瀏覽器運行機制。
四、闡述Promise的一些靜態方法。

Promise.deferredPromise.allPromise.racePromise.resolvePromise.reject

  • 提問機率:25%(相對基礎的問題,通常在其餘問題回答不是很理想的狀況下提問,或者爲了引出下一個題目而提問)
  • 加分項:越多越好
五、Promise存在哪些缺點。

一、沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。
二、若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
三、吞掉錯誤或異常,錯誤只能順序處理,即使在Promise鏈最後添加catch方法,依然可能存在沒法捕捉的錯誤(catch內部可能會出現錯誤)
四、閱讀代碼不是一眼能夠看懂,你只會看到一堆then,必須本身在then的回調函數裏面理清邏輯。

  • 提問機率:25%(此問題做爲提升題目,出現機率不高)
  • 加分項:越多越合理越好(網上有不少說法,不一一佐證)

(此題目,歡迎你們補充答案)

六、使用Promise進行順序(sequence)處理。

一、使用async函數配合await或者使用generator函數配合yield
二、使用promise.then經過for循環或者Array.prototype.reduce實現。

function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(() => task).then(pushValue);
    }, Promise.resolve());
}
  • 提問機率:90%(我司提問機率極高的題目,即能考察面試者對promise的理解程度,又能考察編程邏輯,最後還有bindreduce等方法的運用)
  • 評分標準:說出任意解決方法便可,其中只能說出async函數和generator函數的能夠獲得20%的分數,能夠用promise.then配合for循環解決的能夠獲得60%的分數,配合Array.prototype.reduce實現的能夠獲得最後的20%分數。
七、如何中止一個Promise鏈?

在要中止的promise鏈位置添加一個方法,返回一個永遠不執行resolve或者rejectPromise,那麼這個promise永遠處於pending狀態,因此永遠也不會向下執行thencatch了。這樣咱們就中止了一個promise鏈。

Promise.cancel = Promise.stop = function() {
      return new Promise(function(){})
    }
  • 提問機率:50%(此問題主要考察面試者羅輯思惟)

(此題目,歡迎你們補充答案)

八、Promise鏈上返回的最後一個Promise出錯了怎麼辦?

catchpromise鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,可是catch方法內部也可能出現錯誤,因此有些promise實現中增長了一個方法donedone至關於提供了一個不會出錯的catch方法,而且再也不返回一個promise,通常用來結束一個promise鏈。

done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }
  • 提問機率:90%(一樣做爲出題率極高的一個題目,充分考察面試者對promise的理解程度)
  • 加分項:給出具體的done()方法代碼實現
九、Promise存在哪些使用技巧或者最佳實踐?

一、鏈式promise要返回一個promise,而不僅是構造一個promise
二、合理的使用Promise.allPromise.race等方法。
三、在寫promise鏈式調用的時候,then方法不傳onRejected函數,只須要在最末尾加一個catch()就能夠了,這樣在該鏈條中的promise發生的錯誤都會被最後的catch捕獲到。若是catch()代碼有出現錯誤的可能,須要在鏈式調用的末尾增長done()函數。

  • 提問機率:10%(出題機率極低的一個題目)
  • 加分項:越多越好

(此題目,歡迎你們補充答案)

至此,我司關於Promise的一些面試題目就列舉完畢了,有些題目的答案是開放的,歡迎你們一塊兒補充完善。總結起來,Promise做爲js面試必問部分仍是相對容易掌握並經過的。

總結

Promise做爲全部js開發者的必備技能,其實現思路值得全部人學習,經過這篇文章,但願小夥伴們在之後編碼過程當中能更加熟練、更加明白的使用Promise。

參考連接:

http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
http://www.javashuo.com/article/p-aqjjleyx-h.html

相關文章
相關標籤/搜索