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的實現以及筆者在日常使用過程中的一些心得體會。

實現 Promise

規範解讀

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

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

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

2、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的狀態傳遞下去。

3、不同的promise實現可以的交互。

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

  • [[Resolve]](promise, x) 函數具體運行規則:

    • 如果 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)
    }
  }
}

第 1 條就是實現完畢了,相對簡單,配合代碼註釋很容易理解。

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

  /**
    * 擁有一個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 其他方法實現

1、catch 方法

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

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

catchpromise鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,但是catch方法內部也可能出現錯誤,所以有些promise實現中增加了一個方法donedone相當於提供了一個不會出錯的catch方法,並且不再返回一個promise,一般用來結束一個promise鏈。

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

finally方法用於無論是resolve還是rejectfinally 的參數函數都會被執行。

  finally(fn) {
    return this.then(value => {
      fn();
      return value;
    }, reason => {
      fn();
      throw reason;
    });
  };
4、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++;
      }
    });
  }
5、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);
      }
    });
  }
6、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 個,根據面試者回答情況會適當增減。

1、簡單介紹下 Promise。

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

2、實現一個簡單的,支持異步鏈式調用的 Promise 類。

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

3、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任務(從事件隊列中獲取)
瀏覽器運行機制可參考這篇文章

4、闡述 Promise 的一些靜態方法。

Promise.deferredPromise.allPromise.racePromise.resolvePromise.reject

5、Promise 存在哪些缺點。

1、無法取消Promise,一旦新建它就會立即執行,無法中途取消。
2、如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
3、吞掉錯誤或異常,錯誤只能順序處理,即便在Promise鏈最後添加catch方法,依然可能存在無法捕捉的錯誤(catch內部可能會出現錯誤)
4、閱讀代碼不是一眼可以看懂,你只會看到一堆then,必須自己在then的回調函數里面理清邏輯。

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

1、使用async函數配合await或者使用generator函數配合yield
2、使用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());
}
7、如何停止一個 Promise 鏈?

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

    Promise.cancel = Promise.stop = function() {
      return new Promise(function(){})
    }
8、Promise 鏈上返回的最後一個 Promise 出錯了怎麼辦?

catchpromise鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,但是catch方法內部也可能出現錯誤,所以有些promise實現中增加了一個方法donedone相當於提供了一個不會出錯的catch方法,並且不再返回一個promise,一般用來結束一個promise鏈。

  done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }
9、Promise 存在哪些使用技巧或者最佳實踐?

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

(此題目,歡迎大家補充答案)

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

總結

Promise 作爲所有 js 開發者的必備技能,其實現思路值得所有人學習,通過這篇文章,希望小夥伴們在以後編碼過程中能更加熟練、更加明白的使用 Promise。

參考鏈接:

http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
https://segmentfault.com/a/1190000016550260

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://segmentfault.com/a/1190000039699000