面試高頻點-Promise解析和實現

輝衛無敵發表於2018-12-12

Promise因為它的呼叫方式使得非同步操作清晰簡單,是現在非同步操作的主要方式。Promise的使用和實現是面試中的高頻問點。這篇文章主要解析Promise規範和一版實現方式。

PromiseA+規範

瞭解Promise首先我們要清楚Promise規範的內容,規範規定了Promise的行為和呼叫方式。這裡是規範原文。下面是翻譯總結:

一個Promise主要代表了一個非同步操作的最終結果。與Promise互動的主要方式是通過promise例項的then方法註冊回撥函式。回撥函式分為兩種,一種接受非同步操作成功時的回撥,接受非同步操作結果值作為引數;第二種是非同步操作失敗時的回撥,接受非同步操作失敗原因為引數。規範主要是詳細描述了then方法的實現細節。

主要要求如下:

狀態

一個promise必須處於這3種狀態之中:pendingfullfilledrejected

  • pending: 可以轉換到fullfilled或者rejected狀態
  • fullfilled: 不能轉換到任何狀態且有一個不可變的結果值
  • rejected: 不能轉換到任何狀態且有一個不可變的失敗原因

then方法

一個promise必須有一個then方法來接受非同步操作的結果值或者失敗原因

一個promise的then應該接收兩個引數,成功回撥函式和失敗回撥函式:

promise.then(onFulfilled, onRejected)
複製程式碼
  • onFulfilled和onRejected都是可選的:

    • 如果onFulfilled不是一個函式,它必須被忽略
    • 如果onRejected不是一個函式,它必須被忽略
  • 如果onFulfilled是函式:

    • 它必須在promise狀態變為fullfilled之後執行,且接受promise的結果值作為第一個引數
    • 它在promise狀態變為fullfilled之前不能執行
    • 它最多被執行一次
  • 如果onRejected是函式:

    • 它必須在promise狀態變為rejected之後執行,且接受promise的失敗原因作為第一個引數
    • 它在promise狀態變為rejected之前不能執行
    • 它最多被執行一次
  • onFulfilled或者onRejected直到執行環境堆疊只包含平臺程式碼時才可以執行

  • onFulfilled或者onRejected必須作為一個獨立的函式被呼叫。(不能作為物件屬性執行或者其他指定this的執行方式,比如call和apply)

  • then方法可以在同一個promise上多次呼叫:

    • promise狀態變為fullfilled時,通過then註冊的所有onFulfilled回撥按照註冊順序依次執行
    • promise狀態變為rejected時,通過then註冊的所有onRejected回撥按照註冊順序依次執行
  • then方法必須返回一個promise:

      promise2 = promise1.then(onFulfilled, onRejected);
    複製程式碼
    • 如果onFulfilled或者onRejected返回x, 則執行Pomise Resolution Procedure[[Resolve]](promise2, x)
    • 如果onFulfilled或者onRejected丟擲一個異常e,promise2必須以e作為失敗原因變成rejected狀態
    • 如果onFulfilled不是一個函式,且promise1fullfilled狀態,則promise2也轉變為fullfilled,且結果值和promise1一樣
    • 如果onRejected不是一個函式,且promise1rejected狀態,則promise2也轉變為rejected,且失敗原因和promise1一樣

The Promise Resolution Procedure

promise resolution procedure表示為[[Resolve]](promise, x),是一個接受一個promise和一個值作為引數的抽象操作。如果一個函式是一個thenablethenable表示有then方法的物件或者函式),則它試圖使promise採用x的狀態。這裡對於thenable的處理使Promise更加通用,能夠相容之前並不符合規範但是有合理then方法的非同步實現。

為了執行[[Resolve]](promise, x),需要執行以下步驟:

  • 如果promisex指向同一個物件,則以TypeError為失敗原因將promise轉變為rejected狀態
  • 如果x是一個promise,則promise採用x的狀態以及相應的結果值或者原因
  • 如果x是一個物件或者函式
    • Let定義then,值為x.then
    • 如果在獲取x.then的過程中丟擲異常e,promisee作為失敗原因變成rejected狀態
    • 如果then是一個函式,以x作為它的this呼叫它,第一個引數為resolvePromise,第二個引數為rejectPromise:
      • 如果resolvePromise以引數y被呼叫的話,則執行[[Resolve]](promise, y)
      • 如果rejectPromise以引數r被呼叫的話,則以r作為失敗原因使promise變成rejected狀態
      • 如果resolvePromiserejectPromise都被呼叫了,或者以同樣的引數被呼叫多次,則以第一次呼叫為準,忽略之後的呼叫
      • 如果呼叫then時丟擲一個異常e:
        • 如果resolvePromiserejectPromise被呼叫了,則忽略
        • 否則,promisee作為失敗原因變成rejected狀態
    • 如果then不是一個函式,則promisex為結果值變為fullfilled狀態
  • 如果x不是一個物件或者函式,則promisex為結果值變為fullfilled狀態

Promise實現

我們可以看出標準還是挺複雜的,我們就一步一步,從簡到繁地實現Promise。標準中並沒有規定Promise如何建立,如何完成狀態轉換,這些我們可以參考ES6的promise用法:

const promise1 = new Promise((resolve, reject) => {

});
promise1.then((value) => {}, (reason)=>{})
複製程式碼

promise的狀態

Promise以類的方式出現,建構函式接受一個函式fn作為引數,fn有兩個引數:

  • resolve,使promise從pending狀態轉換為fullfilled狀態,resolve(value)的引數value為promise的結果值;
  • reject,使promise從pending狀態轉換為rejected狀態,reject(reason)的引數reason為promise的失敗原因。

我們第一版可以先從狀態寫起:

const PENDING = 'pending';
const FULLFILLED = 'fullfilled';
const REJECTED = 'rejected';
class Promise {
  constructor(fn) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    const resolve = (value) => {
      if(this.status === PENDING){
        this.status = FULLFILLED;
        this.value = value;
      }
    }
    const reject = (reason) => {
      if(this.status === PENDING){
        this.status = REJECTED;
        this.reason = reason;
      }
    }
    try{
      fn(resolve, reject);
    }catch(e){
      reject(e);
    }
  }
}
複製程式碼

try...catch...是為了捕獲使用者自定義函式fn的錯誤,如果fn執行出錯,則promise會進入rejected狀態。

then函式的兩個函式引數的執行

接下來我們考慮then函式,then函式比較複雜,我們先考慮then(onFulfilled, onRejected)函式中兩個函式引數的執行問題,把then函式的返回值問題放在後面。

then函式中兩個函式引數的執行情況分為3種:

  • promise處於pending狀態,那在then函式中不執行兩個函式引數,等到fn中的非同步操作有結果了,再根據成功或者失敗的結果去執行

  • promise處於fullfilled狀態,則直接執行then函式的第一個函式引數onFulfilled

  • promise處於rejected狀態,則直接執行then函式的第二個函式引數onRejected

class Promise {
  constructor(fn) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.fullFilledCallbacks = []; // 儲存狀態轉變之前的註冊的onFulfilled
    this.rejectedCallbacks = []; // 儲存狀態轉變之前的註冊的onRejected
    const resolve = (value) => {
      if(this.status === PENDING){
        this.status = FULLFILLED;
        this.value = value;
        this.fullFilledCallbacks.forEach(callback => callback(value));
      }
    }
    const reject = (reason) => {
      if(this.status === PENDING){
        this.status = REJECTED;
        this.reason = reason;
        this.rejectedCallbacks.forEach(callback => callback(reason));
      }
    }
    try{
      fn(resolve, reject);
    }catch(e){
      reject(e);
    }
  }

  then(onFulfilled, onRejected){
    if(typeof onFullfilled !== 'function'){
      onFullfilled = () => {};
    }
    if(typeof onRejected !== 'function'){
      onRejected = () => {};
    }

    if (this.status === 'fullfilled'){
      onFulfilled(this.value);
    } else if (this.status === 'rejected'){
      onRejected(this.reason);
    } else {
      this.fullFilledCallbacks.push(onFulfilled);
      this.rejectedCallbacks.push(onRejected);
    }
  }
}
複製程式碼

以上程式碼很簡單,我們藉助fullFilledCallbacksrejectedCallbacks兩個陣列儲存在promise狀態為發生轉變之前通過then方法註冊的onFulfilledonRejected回撥。

then的返回值

由PromiseA+規範可以,then方法必須返回一個promise:

  promise2 = promise1.then(onFulfilled, onRejected);
複製程式碼

且promise2的狀態由onFullfilled或者onRejected的返回值決定,如何進行轉換則由promise resolution procedure方法來實現。我們先不考慮promise resolution procedure,根據規範實現如下:

  then(onFulfilled, onRejected){
    let _resolve;
    let _reject;
    const promise2 = new Promise((resolve, reject) => {
      _resolve = resolve;
      _reject = reject;
    });

    // 如果`onFulfilled`不是一個函式,且`promise1`為`fullfilled`狀態,則`promise2`也轉變為`fullfilled`,且結果值和`promise1`一樣
    if(typeof onFullfilled !== 'function'){
      onFullfilled = (value) => value;
    }

    // 如果`onRejected`不是一個函式,且`promise1`為`rejected`狀態,則`promise2`也轉變為`rejected`,且失敗原因和`promise1`一樣
    if(typeof onRejected !== 'function'){
      onRejected = (reason) => throw new Error(reason);
    }

    // 如果`onFulfilled`或者`onRejected`返回`x`, 則執行Pomise Resolution Procedure`[[Resolve]](promise2, x)`
    const excuteFullfilled = () => {
      const x = onFulfilled(this.value);
      this.promiseResolution(promise2, x, _resolve, _reject);
    }
    const excuteRejected = () => {
      const x = onRejected(this.reason);
      this.promiseResolution(promise2, x, _resolve, _reject);
    }

    // 如果`onFulfilled`或者`onRejected`丟擲一個異常`e`,`promise2`必須以`e`作為失敗原因變成`rejected`狀態
    try{
      if (this.status === 'fullfilled'){
        excuteFullfilled();
      } else if (this.status === 'rejected'){
        excuteRejected();
      } else {
        this.fullFilledCallbacks.push(() => {
          try{
           excuteFullfilled();
          }catch(e){
            _reject(e);
          }
        });
        this.rejectedCallbacks.push(() => {
          try{
            excuteRejected();
          }catch(e){
            _reject(e);
          }
        });
      }
    }catch(e){
      _reject(e);
    }

    return promise2;
  }

  promiseResolution(){
    ...
  }
複製程式碼

接下來我們來實現promiseResolution函式,如果已經忘了可以去複習一下promiseResolution的規範。promiseResolution的主要任務是根據onFulFilled或者onRejected的返回值x來決定promise2的狀態轉變。

promiseResolution(promise, x, resolve, reject){
  // 如果`resolvePromise`和`rejectPromise`都被呼叫了,或者以同樣的引數被呼叫多次,則以第一次呼叫為準,忽略之後的呼叫
  let called = false;

  // 如果`resolvePromise`以引數`y`被呼叫的話,則執行`[[Resolve]](promise, y)`
  const resolvePromise = (y) => {
    if(!called){
      this.promiseResolution(promise, y, resolve, reject);
      called = true;
    }
  }

  // 如果`rejectPromise`以引數`r`被呼叫的話,則以`r`作為失敗原因使``變成`rejected`狀態
  const rejectPromise = (r) => {
    f(!called){
      reject(r);
      called = true;
    }
  }

  if(promise === x){
    throw new Error('TypeError');
  }

  // 如果`x`是一個promise,則`promise`採用`x`的狀態以及相應的結果值或者原因
  if( x instanceof Promise){
    x.then(resovle, reject)
  }

  if( x && (typeof x === 'object' || typeof x === 'function')){
    let then = x.then;
    if(typeof then === 'function'){
      try{
        then.call(x, resolvePromise, rejectPromise);
      }catch(e){
        if(!called){
          reject(e);
        }
      }
    }else{
      // 如果`then`不是一個函式,則`promise`以`x`為結果值變為`fullfilled`狀態
      resolve(x);
    }
  } else {
    // 如果`x`不是一個物件或者函式,則`promise`以`x`為結果值變為`fullfilled`狀態
    resolve(x);
  }

}
複製程式碼

非同步問題

規範中有一條時,onFulfilled或者onRejected直到執行環境堆疊只包含平臺程式碼時才可以執行。規範給出的註釋是:平臺程式碼是指引擎、執行環境和Promise實現程式碼,就是說js主棧中不能有其他程式碼,這就是要求我們非同步執行onFulfilled或者onRejected;我們可以用巨集任務(setTimeout 或者 setImmediate)或者微任務(MutationObserver or process.nextTick)機制來實現。

我們知道,ES6的promise的then方法回撥非同步執行的機制是微任務。在瀏覽器環境我們可以使用MutationObserver來模擬微任務機制,如果瀏覽器不支援MutationObserver,我們回退到setTimeout使用巨集任務實現。

function isNative(fn){
  return fn.toString.includes('native code');
}

function asynTask(){
  if(typeof MutationObserver === 'function' && isNative(MutationObserver)){
    return (fn) => {
      var targetNode = document.createElement('div');
      var config = { attributes: true };
      // Create an observer instance linked to the callback function
      var observer = new MutationObserver(fn);
      // Start observing the target node for configured mutations
      observer.observe(targetNode, config);
      targetNode.id = 'anyway';
    }
  } else if(typeof setImmediate === 'function' && isNative(setImmediate)){
    return (fn) => {setImmediate(fn)};
  } else{
    return (fn) => {setTimeout(fn, 0)};
  }
}
class Promise{
  registerAsyn: asynExcute()
}
複製程式碼

我們使用非同步機制來重寫then

then(onFulfilled, onRejected){
    let _resolve;
    let _reject;
    const promise2 = new Promise((resolve, reject) => {
      _resolve = resolve;
      _reject = reject;
    });

    // 如果`onFulfilled`不是一個函式,且`promise1`為`fullfilled`狀態,則`promise2`也轉變為`fullfilled`,且結果值和`promise1`一樣
    if(typeof onFullfilled !== 'function'){
      onFullfilled = (value) => value;
    }

    // 如果`onRejected`不是一個函式,且`promise1`為`rejected`狀態,則`promise2`也轉變為`rejected`,且失敗原因和`promise1`一樣
    if(typeof onRejected !== 'function'){
      onRejected = (reason) => throw new Error(reason);
    }

    // 如果`onFulfilled`或者`onRejected`返回`x`, 則執行Pomise Resolution Procedure`[[Resolve]](promise2, x)`
    const excuteFullfilled = () => {
      try{
        const x = onFulfilled(this.value);
        this.promiseResolution(promise2, x, _resolve, _reject);
      }catch(e){
        _reject(e);
      }
    }
    const excuteRejected = () => {
      try{
        const x = onRejected(this.reason);
        this.promiseResolution(promise2, x, _resolve, _reject);
      }catch(e){
        _reject(e);
      }
    }

    if (this.status === 'fullfilled'){
      registerAysn(excuteFullfilled);
    } else if (this.status === 'rejected'){
      registerAysn(excuteRejected);
    } else {
      this.fullFilledCallbacks.push(() => {
        registerAysn(excuteFullfilled);
      });
      this.rejectedCallbacks.push(() => {
        registerAysn(excuteRejected);
      });
    }

    return promise2;
  }

複製程式碼

目前為止我們實現了Promise的基本功能。

Promise靜態方法和其他例項方法

class Promise{
  catch(fn) {
    return this.then((null, fn);
  }

  finally(fn) {
    const P = this.constructor;
    return this.then(
      value  => P.resolve(fn()).then(() => value),
      reason => P.resolve(fn()).then(() => { throw reason })
    );
  }
}

//resolve方法

Promise.resolve = function(value) {
  if (value instanceof Promise) {
    return value;
  }

  return new Promise(function(resolve) {
    resolve(value);
  });
};

Promise.reject = function(value) {
  return new Promise(function(resolve, reject) {
    reject(value);
  });
};

Promise.race = function(promises) {
  return new Promise(function(resolve, reject) {
    for (var i = 0, len = promises.length; i < len; i++) {
      promises[i].then(resolve, reject);
    }
  });
};

Promise.all = function(promises){
  if (!promises || typeof promises.length === 'undefined')
      throw new TypeError('Promise.all should accepts an array');
  let values = [];
  let i = 0;
  let length = promises.length;
  function processData(index,data){
    values[index] = data;
    i++;
    if(i == length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
}

複製程式碼

參考來源

juejin.im/post/5b2f02…

github.com/taylorhakes…

相關文章