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

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

這篇文章將一步步實現 Promise,完全弄懂 Promise。面試

Promise 基本構成

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

最重要的還有個 then 的方法,then 函數能夠傳入兩個函數做爲參數,第一個函數用來獲取異步操做的結果,第二個函數用來獲取錯誤的緣由。promise

除此以外還須要 valuereason 存放 Promise 的結果或錯誤緣由。markdown

從上面這些信息能夠轉化爲下面的代碼:異步

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 函數有可能會調用屢次,那麼能夠用數組保存參數。oop

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 函數的鏈式調用。this

實現鏈式調用

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】

前端develop
相關文章
相關標籤/搜索