【面試題解析】手動實現Promise

前端面試的時候,經常能看到這樣一道題,實現一個Promise

這篇文章將一步步實現 Promise,徹底弄懂 Promise。

Promise 基本構成

平時使用 Promise 我們可以知道 Promise 存在三種狀態 Pending、Resolve、Reject,在 new Promise 時需要傳入一個函數, 參數爲 resolvereject 的函數,這兩個函數用來改變 Promise 的狀態。

最重要的還有個 then 的方法,then 函數可以傳入兩個函數作爲參數,第一個函數用來獲取異步操作的結果,第二個函數用來獲取錯誤的原因。

除此之外還需要 valuereason 存放 Promise 的結果或錯誤原因。

從上面這些信息可以轉化爲下面的代碼:

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = null;
    this.reason = null;

    function resolve (value) {
      this.status = RESOLVED;
      this.value = value;
    };

    function reject (reason) {
      this.status = REJECTED;
      this.reason = reason;
    };

    executor(resolve.bind(this), reject.bind(this));
  }

  then(onFulfilled, onRejected) {
    if (this.status === RESOLVED) {
      onFulfilled(this.value);
    }

    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}

Promise 的狀態只允許修改一次,那麼 resolvereject 需要加上狀態判斷。

function resolve (value) {
  if (this.status !== PENDING) return;
  this.status = RESOLVED;
  this.value = value;
};

function reject (reason) {
  if (this.status !== PENDING) return;
  this.status = REJECTED;
  this.reason = reason;
};

在調用 then 函數時,Promise 的狀態有可能還是 Pending 的狀態,這時需要將 then 函數的兩個參數進行保存,狀態改變時在進行調用。then 函數有可能會調用多次,那麼可以用數組保存參數。

class Promise {
  constructor(executor) {
    // ...
    this.resolveCbs = [];
    this.rejectCbs = [];
    function resolve (value) {
      // ...
      this.resolveCbs.map(fn => fn(this.value));
    };

    function reject (reason) {
      // ...
      this.rejectCbs.map(fn => fn(this.reason));
    };
  }

  then(onFulfilled, onRejected) {
    // ...
    if (this.status === PENDING) {
      this.resolveCbs.push(onFulfilled);
      this.rejectCbs.push(onRejected);
    }
  }
}

寫到這裏,一個最基本的 Promise 就可以使用了。

 new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
}).then(res => {
  console.log(res);
});

上面的代碼雖然完成了最基本的 Promise,但是還未實現 then 函數的鏈式調用。

實現鏈式調用

new Promise((resolve, reject) => {
  // ...
}).then(res => {
  // ...
}).then(res => {
  // ...
})

鏈式調用也是 Promise 的重點所在,因爲有了鏈式調用,才能避免回調地獄的問題。接下來就來一步步實現。

then 是 Promise 的方法,爲了能夠繼續調用 then 函數,需要 then 函數返回一個新的 Promise。

onFulfilledonRejected 的返回值有可能也是一個 Promise,那麼需要等待 Promise 執行完的結果傳遞給下一個 then 函數。如果返回的不是 Promise,就可以將結果傳遞給下一個 then 函數。

then 函數進行如下修改,resolvePromise 另外實現。

class Promise {
  // ...
  then(onFulfilled, onRejected) {
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === RESOLVED) {
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

      if (this.status === REJECTED) {
        let x = onRejected(this.reason);
        resolvePromise(promise2, x, resolve, reject);
      }

      if (this.status === PENDING) {
        this.resolveCbs.push(() => {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        });

        this.rejectCbs.push(() => {
          let x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        });
      }
    });

    return promise2;
  }
}

實現 resolvePromise

then(onFulfilled, onRejected) {
  function resolvePromise (promise2, x, resolve, reject) {
    if (promise2 === x) {
      // 不允許 promise2 === x; 避免自己等待自己
      return reject(new TypeError('Chaining cycle detected for promise'));
    }

    // 防止重複調用
    let called = false;

    try {
      if (x instanceof Promise) {
        let then = x.then;
        // 第一個參數指定調用對象
        // 第二個參數爲成功的回調,將結果作爲 resolvePromise 的參數進行遞歸
        // 第三個參數爲失敗的回調
        then.call(x, y => {
          if (called) return;
          called = true;
          // resolve 的結果依舊是 Promise 那就繼續解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if (called) return;
          called = true;
          reject(err);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      reject(e);
    }
  }

  // ...
}

優化 then 函數

then 函數的 onFulfilledonRejected 參數允許不傳.

Promise/A+ 規範要求 onFulfilledonRejected 不能被同步調用,可以使用 setTimeout 改爲異步調用。

then(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => { return v };
  onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };

  function resolvePromise (promise2, x, resolve, reject) {...}

  let promise2 = new Promise((resolve, reject) => {
    function fulfilled () {
      setTimeout(() => {
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }, 0);
    };

    function rejected () {
      setTimeout(() => {
        let x = onRejected(this.reason);
        resolvePromise(promise2, x, resolve, reject);
      }, 0);
    }

    if (this.status === RESOLVED) {
      fulfilled.call(this);
    }

    if (this.status === REJECTED) {
      rejected.call(this);
    }

    if (this.status === PENDING) {
      this.resolveCbs.push(fulfilled.bind(this));
      this.rejectCbs.push(rejected.bind(this));
    }
  });

  return promise2;
}

catch 等方法實現

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

  static resolve (val) {
    return new Promise((resolve) => {
      resolve(val);
    });
  }

  static reject (val) {
    return new Promise((resolve, reject) => {
      reject(val);
    });
  }

  static race(promises) {
    return new Promise((resolve, reject) => {
      promises.map(promise => {
        promise.then(resolve, reject);
      });
    });
  }

  static all(promises) {
    let arr = [];
    let i = 0;
    return new Promise((resolve, reject) => {
      promises.map((promise, index) => {
        promise.then(data => {
          arr[index] = data;
          if (++i === promises.length) {
            resolve(arr);
          }
        }, reject);
      })
    })
  }
}

參考文章

BAT前端經典面試問題:史上最最最詳細的手寫Promise教程


如果你喜歡我的文章,希望可以關注一下我的公衆號【前端develop】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章