實現Promise,遵循Promise/A+規範

Promise 是異步編程的一種解決方案,比傳統的解決方案(回調函數和事件)更合理和更強大。如今前端應用中Promise已經獲得了普遍使用。本文經過實現符合Promise/A+規範的Promise,對其加深印象。javascript

構造函數

咱們在使用Promise時,一般是使用new操做符進行構造,傳入resolver函數,該函數會接受成功(resolve)、失敗(reject)的回調函數,當咱們肯定結果時,須要調用resolve或reject,具體代碼以下:前端

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000)
})
// 1s後控制檯打印success
p1.then(res => console.log(res))
複製代碼

因此咱們的Promise也須要是個構造函數,而且執行用戶傳入的resolver函數,將定義好的回調函數傳進去。下面是具體的代碼:java

注:本文代碼的實現,下劃線開頭表明私有屬性、私有方法。git

// 定義Promise的三種狀態常量
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

function Promise(resolver) {
  // 傳入的必須是函數
  if (typeof resolver !== 'function') {
    throw new TypeError('Promise resolver ' + resolver + ' is not a function');
  }
  
  // resolve或reject的結果值
  this._result = undefined;
  // 狀態
  this._status = PENDING;

  try {
    // 執行
    resolver(this._resolve.bind(this), this._reject.bind(this));
  } catch (error) {
    // 捕獲錯誤
    this._reject(error);
  }
}
// 私有方法,傳給resolver的成功、失敗回調
Promise.prototype._resolve = function() {}
Promise.prototype._reject = function() {}
複製代碼

接下來咱們來實現_resolve、_reject私有方法,其實邏輯很簡單,咱們只須要改變Promise狀態,以及成功的值或者失敗的緣由。但要注意Promise一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果,因此咱們只有在狀態時PENDING的時纔會執行。es6

Promise.prototype._resolve = function(value) {
  // setTimeout 爲了異步執行
  setTimeout(() => {
    if (this._status !== PENDING) return;
    this._status = FULFILLED;
    this._result = value;
  });
}

Promise.prototype._reject = function (reason) {
  // setTimeout 爲了異步執行
  setTimeout(() => {
    if (this._status !== PENDING) return;
    this._status = REJECTED;
    this._result = reason;
  });
};
複製代碼

Promise.prototype.then

Promise的核心就是then方法,Promise/A+規範大都也是針對then方法進行闡述,實現了then方法後,咱們再來實現其餘API就方便了不少。github

Promise的then方法的規範有以下幾點編程

  1. 接受兩個參數,onFulfilled(成功回調), onRejected(失敗回調),當回調不是函數時, 其必須被忽略,支持透傳promise

  2. then 方法能夠被同一個 Promise 調用屢次異步

    • 當 Promise 成功執行時,全部 onFulfilled 需按照其註冊順序依次回調
    • 當 Promise 被拒絕執行時,全部的 onRejected 需按照其註冊順序依次回調
  3. then 方法必須返回一個 Promise 對象異步編程

    • 若是 onFulfilled 或者 onRejected 返回一個值 x ,則運行下面的Promise解決過程
    • 若是 onFulfilled 或者 onRejected 拋出一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e

咱們針對上述幾點分別來實現一下

關於第一點,能夠控制檯執行下面代碼

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000)
})
// 1s後控制檯打印success
p1.then(1).then(res => console.log(res))
複製代碼

第二個then方法依然能夠接受到resolve成功的值,因此當then方法傳入的不是函數時,咱們要規範使其變成函數支持透傳。

Promise.prototype.then = function (onFulfilled, onRejected) {
  // 保證是函數,不是函數要實現透傳
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
  onRejected = typeof onRejected === 'function' ? onRejected : (v) => { throw v };
}
複製代碼

實現第一點很簡單,咱們只須要吧把結果/錯誤 -> 返回/拋出傳遞給出去就能夠啦。

咱們再來看第二點,爲了能夠屢次調用而且依次執行,咱們須要改下以前寫過的代碼,咱們須要增長倆個回調隊列,成功、失敗各一個。其實也能夠用一個隊列來存儲,我這裏採用的分別存儲。

function Promise(resolver) {
  // 忽略無關代碼...

  // resolve的回調隊列
+ this._resolveCbs = [];
  // reject的回調隊列
+ this._rejectCbs = [];
}

Promise.prototype._resolve = function(value) {
  setTimeout(() => {
    // 忽略無關代碼...
+ this._resolveCbs.forEach((callback) => callback(value));
  });
}

Promise.prototype._reject = function(reason) {
  setTimeout(() => {
    // 忽略無關代碼...
+ this._rejectCbs.forEach((callback) => callback(reason));
  });
}
複製代碼

咱們在then方法中,若是狀態還處於PENDING,就須要將傳入的onFulfilled(成功回調), onRejected(失敗回調)插入對應的隊列中,不然直接執行就好。這也就是咱們要實現第三點的核心邏輯。

根據第三點所述,咱們老是須要執行onFulfilled 或 onRejected,而後傳入Promise解決過程,此外還須要捕獲這個過程,直接reject。具體核心代碼以下

let promise = undefined;
return (promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    try {
      var x = onFulfilled(this._result);  // 或者 var x = onRejected(this._result);
      // resolvePromise Promise解決過程 下一段講
      resolvePromise(promise, x, resolve, reject);
    } catch (e) {
      return reject(e);
    }
  });
}));
複製代碼

下面咱們就是把這段代碼分別用在 PENDING, FULFILLED, REJECTED三種狀態,完整代碼以下:

Promise.prototype.then = function (onFulfilled, onRejected) {
  // 保證是函數,不是函數要實現透傳
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (v) => {
          throw v;
        };

  let promise = undefined;

  // 已經resolve
  if (this._status === FULFILLED) {
    return (promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try {
          var x = onFulfilled(this._result);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
  // 已經reject
  if (this._status === REJECTED) {
    return (promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try {
          var x = onRejected(this._result);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
  // pending時直接放入回調隊列中,放入隊列彙總不須要加setTimeout,由於執行時候已是setTimeout中
  if (this._status === PENDING) {
    return (promise = new Promise((resolve, reject) => {
      this._resolveCbs.push((value) => {
        try {
          var x = onFulfilled(value);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
      this._rejectCbs.push((reason) => {
        try {
          var x = onRejected(reason);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
};
複製代碼

上面代碼看似不少,很複雜,但其實根據規範來看,其實很簡單,並且有大量的重複代碼。那咱們還有一個resolvePromise函數沒有完成,接下來我但願讀者能夠本身去讀一下Promise 解決過程的邏輯。點擊連接去查看。由於函數的實現徹底照規範的邏輯書寫,沒有技巧可言。

這裏簡單的總結幾點

  1. 爲了和其餘promise並存,咱們不能只判斷onFulfilled,onRejected函數返回的是不是promise,咱們只須要保證其返回值存在then方法就去嘗試按promise處理。

  2. 若是沒有then屬性,或者then屬性不是函數的話,直接按照resolve(x)處理

  3. 若是then存在而且是函數,按照promise處理的同時,須要捕獲錯誤按reject(x)處理

  4. 若是傳入的回調均被調用,或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用(只能調用一次,須要有標誌位)

按照上述四點,咱們能夠寫出resolvePromise函數的代碼

function resolvePromise(promise, x, resolve, reject) {
  // 若是 promise 和 x 指向同一對象,以 TypeError 爲據因拒絕執行 promise
  if (x === promise) {
    return reject(new TypeError('Chaining cycle detected for promise!'));
  }
  // 用於 「優先採用首次調用並忽略剩下的調用」的標誌位
  let invoked = false;
  // 嘗試把 x.then 賦值給 then
  let then = undefined;
  // x 爲對象或函數
  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    try {
      then = x.then;
      if (typeof then === 'function') {
        // 若是 then 是函數,將 x 做爲函數的做用域 this 調用之
        then.call(
          x,
          (y) => {
            if (invoked) return;
            invoked = true;
            return resolvePromise(promise, y, resolve, reject);
          },
          (r) => {
            if (invoked) return;
            invoked = true;
            return reject(r);
          }
        );
      } else {
        // 若是 then 不是函數,以 x 爲參數執行 promise
        return resolve(x);
      }
    } catch (e) {
      // 若是取 x.then 的值時拋出錯誤 e ,則以 e 爲據因拒絕 promise
      if (invoked) return;
      invoked = true;
      return reject(e);
    }
  } else {
    // 若是 x 不爲對象或者函數,以 x 爲參數執行 promise
    return resolve(x);
  }
}
複製代碼

Promise.prototype.catch

.catch()發生錯誤時的回調函數 至關於使用.then(null, onRejected),咱們實現了then方法,因此catch方法就至關簡單

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};
複製代碼

Promise.prototype.finally

.finally()方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
複製代碼

上面代碼中,無論promise最後的狀態,在執行完then或catch指定的回調函數之後,都會執行finally方法指定的回調函數。

那咱們就能夠經過then方法,傳入的成功、失敗回調函數中都去執行callback

Promise.prototype.finally = function (callback) {
  return this.then(
    (value) => Promise.resolve(callback()).then(() => value),
    (reason) =>
      Promise.resolve(callback()).then(() => {
        throw reason;
      })
  );
};
複製代碼

注意咱們要.then將結果透傳,由於finally後面還能夠繼續調用then方法。

// 最後一個then理應接受到2做爲參數
Promise.resolve(2).finally(() => { }).then(res => console.log(res))
複製代碼

Promise.resolve

有時須要將現有對象轉爲 Promise 對象,Promise.resolve()方法就起到這個做用。

咱們只須要把then方法中的成功邏輯拿出來使用就能夠。(其中x不是onFulfilled執行的值,直接是傳入的參數)

Promise.resolve = function (value) {
  let promise;
  return (promise = new Promise((resolve, reject) => {
    resolvePromise(promise, value, resolve, reject);
  }));
};
複製代碼

Promise.reject

Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected。

Promise.reject = function (reason) {
  return new Promise((_, reject) => reject(reason));
};
複製代碼

Promise.all

/** * Promise.all()方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。 * 只有全部實例的狀態都變成fulfilled,新的實例狀態纔會變成fulfilled,新實例參數 * 只要其中有一個被rejected,新的實例就變成rejected,此時第一個被reject的實例的返回值,會傳遞給新實例的回調函數。 */
Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    let resolvedCount = 0;
    let promiseCount = promises.length;
    let resolvedValues = new Array(promiseCount);
    for (let i = 0; i < promiseCount; i++) {
      Promise.resolve(promises[i]).then(
        (value) => {
          resolvedCount++;
          resolvedValues[i] = value;
          // 數量相同說明promise實例都是成功
          if (resolvedCount == promiseCount) {
            return resolve(resolvedValues);
          }
        },
        (reason) => {
          // 率先reject的直接失敗,傳入緣由
          return reject(reason);
        }
      );
    }
  });
};
複製代碼

Promise.race

/** * Promise.race()方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。 * 只要其中有一個狀態變動,新的實例就跟隨着變動,參數會傳遞給新實例的回調函數。 */
Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (var i = 0; i < promises.length; i++) {
      // 誰快誰說了算!
      Promise.resolve(promises[i]).then(
        (value) => {
          return resolve(value);
        },
        (reason) => {
          return reject(reason);
        }
      );
    }
  });
};
複製代碼

總結

Promise的實現難點主要集中在then方法上,其餘方法都是基於then方法實現的。實現一個Promise也是筆試的高頻題目,但願本文章能夠給你帶來幫助。

完整源碼傳送門

參考

相關文章
相關標籤/搜索