自己動手寫Promise

笨笨小撒發表於2018-08-24

自己動手寫Promise

相信作為一名JSer,大家如今肯定都對Promise的使用非常熟悉了。Promise的出現,大大改善了js程式碼『回撥地獄』的問題,再結合async/await等語法特性,可以讓JS書寫簡潔優美、可讀性高的非同步程式碼。

在Promise規範化的路上,社群的貢獻可謂至關重要。早期各種版本的Promise庫最終推動了Promises/A+規範的達成,並最終被納入語言規範。如今隨著async/await的出現,使得JS中的非同步程式設計變得更加優雅、簡潔。

今天我準備和大家一起,嘗試自己動手實現一個簡略版本的Promise的polyfill。

自己動手寫Promise

Promise建構函式與狀態

我們的Promise將處於以下三種狀態中的一種:

  • PENDING:待定狀態,也是Promise的初始狀態
  • FULFILLED:已滿足,即該Promise已被resolve
  • REJECTED:已拒絕,即該Promise已被reject

PENDING狀態可以轉換為FULFILLED或REJECTED狀態,而後兩者不能再次轉換。

const STATUS = {
  PENDING: Symbol('PENDING'),
  FULFILLED: Symbol('FULFILLED'),
  REJECTED: Symbol('REJECTED'),
}
複製程式碼

在Promise建構函式中,我們會初始化一些屬性:將狀態置為PENDING,初始化回撥陣列。

我們將接收一個函式作為引數executor,隨後這個函式將立即被呼叫,同時傳入兩個函式作為引數,呼叫這兩個函式將分別resolve和reject當前promise:

class Promise {
  constructor(executor) {
    this.status = STATUS.PENDING;
    this.handlers = [];
    this._resolveFromExecutor(executor);
  }
}
複製程式碼

執行executor

接著我們執行executor,要注意的是執行時我們要使用try/catch,如果發生異常,則使用丟擲的異常reject當前promise。executor也可以主動resolve或reject當前promise:

_resolveFromExecutor(executor) {
  const r = this._execute(executor, (value) => {
    this._resolveCallback(value);
  }, (reason) => {
    this._rejectCallback(reason);
  });

  if (r !== undefined) {
      this._rejectCallback(r);
  }
}
複製程式碼

resolve與reject

_resolveCallback與_rejectCallback都需要率先判斷當前promise的狀態是否為PENDING:若狀態非PENDING則直接忽視呼叫;否則設定狀態為FULFILLED或REJECTED,並且將值或拒絕原因記錄下來,同時非同步處理回撥:

_resolveCallback(value) {
  if (this.status !== STATUS.PENDING) return;
  return this._fulfill(value);
}

_fulfill(value) {
  if (this.status !== STATUS.PENDING) return;
  this.status = STATUS.FULFILLED;
  this._value = value;

  async.settlePromises(this);
}

_rejectCallback(reason) {
  this._reject(reason);
}

_reject(reason) {
  if (this.status !== STATUS.PENDING) return;
  this.status = STATUS.REJECTED;
  this._reason = reason;

  async.settlePromises(this);
}
複製程式碼

非同步處理

這裡的async.settlePromises會非同步呼叫promise._settlePromises。

js有許多非同步執行的方式,包括簡單的setTimeout、requestAnimationFrame,node環境下的nextTick、setImmediate,還有一些方法比如利用圖片載入error或是MutationObserver等等。這裡偷個懶直接用setTimeout了。

const async = {
  schedule(fn) {
    setTimeout(fn, 0);
  },
  settlePromises(promise) {
    this.schedule(() => {
      promise._settlePromises();
    });
  },
};
複製程式碼

_settlePromises將逐個執行handlers陣列中註冊的回撥,並在此後清空handlers陣列。在此實現_settlePromises方法之前,先來看看是如何向handlers陣列新增回撥的。

then與catch

then與cacth將呼叫_addCallbacks向handlers陣列新增回撥:

_addCallbacks(fulfill, reject, promise) {
  this.handlers.push({
    fulfill,
    reject,
    promise,
  });
}
複製程式碼

而then與catch是對私有方法_then的進一步包裝:

then(didFulfill, didReject) {
  return this._then(didFulfill, didReject);
}

catch(fn) {
  return this.then(undefined, fn);
}
複製程式碼

每當呼叫_then方法將生成一個新的promise例項並返回:

_then(didFulfill, didReject) {
  const promise = new Promise(INTERNAL);
  let handler;
  let value;

  this._addCallbacks(didFulfill, didReject, promise);

  return promise;
}
複製程式碼

這裡我們傳入的executor將不會呼叫resolve或reject改變promise狀態,而是將其加入父級promise的handlers陣列並在父級_settlePromises時處理,由此形成了promise鏈:

parentPromise.then -> 生成childPromise並返回 -> 加入parentPromise的handlers

parentPromise._settlePromises -> 執行childPromise的_fulfill或_reject
複製程式碼

_settlePromises

_settlePromises會遍歷handlers並呼叫_settlePromise。如果_then加入了回撥函式,那我們需要呼叫這個函式並根據其結果去resolve或reject目標promise;否則直接用原本的結果來resolve或reject目標promise:

_settlePromises() {
  this.handlers.forEach(({ fulfill, reject, promise }) => {
    if (this.status === STATUS.FULFILLED) {
      this._settlePromise(promise, fulfill, this._value);
    } else {
      this._settlePromise(promise, reject, this._reason);
    }
  });
  this.handlers.length = 0;
}

_settlePromise(promise, handler, value) {
  if (typeof handler === 'function') {
      this._settlePromiseFromHandler(handler, value, promise);
  } else {
    if (promise.status === STATUS.FULFILLED) {
      promise._fulfill(value);
    } else {
      promise._reject(value);
    }
  }
}

_settlePromiseFromHandler(handler, value, promise) {
  const x = tryCatch(handler).call(null, value);

  if (x === errorObj) {
    promise._reject(x.e);
  } else {
    promise._resolveCallback(x);
  }
}
複製程式碼

Promise.resolve與Promise.reject

接著新增兩個靜態方法,返回一個promise示例,並立刻用傳入的值resolve或reject這個promise。

Promise.resolve = function resolve(v) {
  return new Promise((res) => {
    res(v);
  });
};

Promise.reject = function reject(v) {
  return new Promise((_, rej) => {
    rej(v);
  });
};
複製程式碼

立即執行回撥

當然,以上的程式碼並不會正確執行。

首先我們來看一下_then方法。我們需要判斷當前promise是否是PENDING狀態:如果是則將回撥加入handlers陣列;否則立即執行回撥:

const async = {

  ...

  invoke(fn, receiver, arg) {
    this.schedule(() => {
      fn.call(receiver, arg);
    });
  },
};

_then(didFulfill, didReject) {
  const promise = new Promise(INTERNAL);
  const target = this;
  let handler;
  let value;

  if (target.status !== STATUS.PENDING) {
    if (target.status === STATUS.FULFILLED) {
      handler = didFulfill;
      value = target._value;
    } else if (target.status === STATUS.REJECTED) {
      handler = didReject;
      value = target._reason;
    }

    async.invoke(
      function ({ promise, handler, value }) {
        this._settlePromise(promise, handler, value);
      },
      target,
      {
        handler,
        promise,
        value,
      }
    );
  } else {
    target._addCallbacks(didFulfill, didReject, promise);
  }

  return promise;
}
複製程式碼

當resolve的值為promise例項

接下來還有一個問題要處理,如果一個promise被另一個promise所resolve,則需要進行特別的處理。

如果作為值的promise已經非PENDING狀態,那比較簡單,直接用它的結果resolve或reject當前的promise即可。如果目標promise還在PENDING狀態,則將當前的promise以及它的handlers轉交給目標promise。因為當前的promise可能也被作為其他promise的resolve的值,因此這裡也要維護一個上級狀態,以便找到鏈的最前端:

_resolveCallback(value) {
  if (this.status !== STATUS.PENDING) return;
  if (!(value instanceof Promise)) return this._fulfill(value);

  const p = value._target();

  if (p.status === STATUS.PENDING) {
    const len = this.handlers.length;
    this.handlers.forEach(({ fulfill, reject, promise }) => {
      p._addCallbacks(fulfill, reject, promise);
    });
    this._isFollowing = true;
    this.handlers.length = 0;
    this._followee = p;
  } else if (p.status === STATUS.FULFILLED) {
    this._fulfill(p._value);
  } else if (p.status === STATUS.REJECTED) {
    this._reject(p._reason);
  }
}

_target() {
  let ret = this;
  while (ret._isFollowing) ret = ret._followee;
  return ret;
}
複製程式碼

同時當我們呼叫promise._then時,也需要使用這個追溯機制:

_then(didFulfill, didReject) {
  const promise = new Promise(INTERNAL);
  const target = this;
  
  ...

}
複製程式碼

Promise.all

最後我們實現一下Promise.all。這裡的思路很簡單,生成一個promise示例,對傳入的陣列中的所有promise用then監聽結果,如果全部resolve則用所有結果組成的陣列resolve返回的promise,有一個失敗則立即用這個錯誤reject:

class PromiseArray {
  constructor(values, count, isAll) {
    this._ps = values;
    this._count = isAll ? values.length : count;
    this._isAll = isAll;
    this._values = [];
    this._valueCount = 0;
    this._reasons = [];
    this._reasonCount = 0;
    this._promise = new Promise(INTERNAL);
    this._iterate();
  }

  _iterate() {
    let p;
    for (let i = 0; i < this._ps.length; i++) {
      p = this._ps[i];
      p.then(function (index, value) {
        if (this._isAll) {
          this._values[index] = value;
        } else {
          this._values.push(value);
        }
        this._valueCount++;
        this._check();
      }.bind(this, i), function (index, reason) {
        if (this._isAll) {
          this._reasons[index] = reason;
        } else {
          this._reasons.push(reason);
        }
        this._reasonCount++;
        this._check();
      }.bind(this, i));
    }
  }

  _check() {
    if (this._count <= this._valueCount) {
      this._promise._fulfill(this._values);
    } else if (this._ps.length - this._count < this._reasonCount) {
      this._promise._reject(this._reasons);
    }
  }
}

Promise.all = function (values) {
  return new PromiseArray(values, undefined, true)._promise;
};
複製程式碼

小結

實現Promise的關鍵點在於如何實現Promise鏈。

使用Promise以及async/await將大大提高程式碼的可讀性、降低複雜度。

完整程式碼

(function () {
  const errorObj = {};
  let tryCatchTarget;

  const tryCatcher = function tryCatcher() {
    try {
      const target = tryCatchTarget;
      tryCatchTarget = null;
      return target.apply(this, arguments);
    } catch (e) {
      errorObj.e = e;
      return errorObj;
    }
  };
  const tryCatch = function tryCatch(fn) {
    tryCatchTarget = fn;
    return tryCatcher;
  };

  const async = {
    schedule(fn) {
      setTimeout(fn, 0);
    },
    invoke(fn, receiver, arg) {
      this.schedule(() => {
        fn.call(receiver, arg);
      });
    },
    settlePromises(promise) {
      this.schedule(() => {
        promise._settlePromises();
      });
    },
  };

  const INTERNAL = function INTERNAL() {};

  const STATUS = {
    PENDING: Symbol('PENDING'),
    FULFILLED: Symbol('FULFILLED'),
    REJECTED: Symbol('REJECTED'),
  }

  class Promise {
    constructor(executor) {
      this.status = STATUS.PENDING;
      this.handlers = [];
      this._isFollowing = false;
      this._followee = null;
      this._resolveFromExecutor(executor);
    }

    _resolveFromExecutor(executor) {
      // if (executor === INTERNAL) return;
      const r = this._execute(executor, (value) => {
        this._resolveCallback(value);
      }, (reason) => {
        this._rejectCallback(reason);
      });

      if (r !== undefined) {
          this._rejectCallback(r);
      }
    }

    _execute(executor, resolve, reject) {
      try {
        executor(resolve, reject);
      } catch (e) {
        return e;
      }
    }

    _resolveCallback(value) {
      if (this.status !== STATUS.PENDING) return;
      if (!(value instanceof Promise)) return this._fulfill(value);

      const p = value._target();

      if (p.status === STATUS.PENDING) {
        const len = this.handlers.length;
        this.handlers.forEach(({ fulfill, reject, promise }) => {
          p._addCallbacks(fulfill, reject, promise);
        });
        this._isFollowing = true;
        this.handlers.length = 0;
        this._followee = p;
      } else if (p.status === STATUS.FULFILLED) {
        this._fulfill(p._value);
      } else if (p.status === STATUS.REJECTED) {
        this._reject(p._reason);
      }
    }

    _target() {
      let ret = this;
      while (ret._isFollowing) ret = ret._followee;
      return ret;
    }

    _fulfill(value) {
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.FULFILLED;
      this._value = value;

      async.settlePromises(this);
    }

    _rejectCallback(reason) {
      this._reject(reason);
    }

    _reject(reason) {
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.REJECTED;
      this._reason = reason;

      async.settlePromises(this);
    }

    then(didFulfill, didReject) {
      return this._then(didFulfill, didReject);
    }

    _then(didFulfill, didReject) {
      const promise = new Promise(INTERNAL);
      const target = this._target();
      let handler;
      let value;

      if (target.status !== STATUS.PENDING) {
        if (target.status === STATUS.FULFILLED) {
          handler = didFulfill;
          value = target._value;
        } else if (target.status === STATUS.REJECTED) {
          handler = didReject;
          value = target._reason;
        }

        async.invoke(
          function ({ promise, handler, value }) {
            this._settlePromise(promise, handler, value);
          },
          target,
          {
            handler,
            promise,
            value,
          }
        );
      } else {
        target._addCallbacks(didFulfill, didReject, promise);
      }

      return promise;
    }

    catch(fn) {
      return this.then(undefined, fn);
    }

    _addCallbacks(fulfill, reject, promise) {
      this.handlers.push({
        fulfill,
        reject,
        promise,
      });
    }

    _settlePromises() {
      this.handlers.forEach(({ fulfill, reject, promise }) => {
        if (this.status === STATUS.FULFILLED) {
          this._settlePromise(promise, fulfill, this._value);
        } else {
          this._settlePromise(promise, reject, this._reason);
        }
      });
      this.handlers.length = 0;
    }

    _settlePromise(promise, handler, value) {
      if (typeof handler === 'function') {
          this._settlePromiseFromHandler(handler, value, promise);
      } else {
        if (promise.status === STATUS.FULFILLED) {
          promise._fulfill(value);
        } else {
          promise._reject(value);
        }
      }
    }

    _settlePromiseFromHandler(handler, value, promise) {
      const x = tryCatch(handler).call(null, value);

      if (x === errorObj) {
        promise._reject(x.e);
      } else {
        promise._resolveCallback(x);
      }
    }
  }

  Promise.resolve = function resolve(v) {
    return new Promise((res) => {
      res(v);
    });
  };
  Promise.reject = function reject(v) {
    return new Promise((_, rej) => {
      rej(v);
    });
  };

  window.Promise = Promise;

  class PromiseArray {
    constructor(values, count, isAll) {
      this._ps = values;
      this._count = isAll ? values.length : count;
      this._isAll = isAll;
      this._values = [];
      this._valueCount = 0;
      this._reasons = [];
      this._reasonCount = 0;
      this._promise = new Promise(INTERNAL);
      this._iterate();
    }

    _iterate() {
      let p;
      for (let i = 0; i < this._ps.length; i++) {
        p = this._ps[i];
        p.then(function (index, value) {
          if (this._isAll) {
            this._values[index] = value;
          } else {
            this._values.push(value);
          }
          this._valueCount++;
          this._check();
        }.bind(this, i), function (index, reason) {
          if (this._isAll) {
            this._reasons[index] = reason;
          } else {
            this._reasons.push(reason);
          }
          this._reasonCount++;
          this._check();
        }.bind(this, i));
      }
    }

    _check() {
      if (this._count <= this._valueCount) {
        this._promise._fulfill(this._values);
      } else if (this._ps.length - this._count < this._reasonCount) {
        this._promise._reject(this._reasons);
      }
    }
  }

  Promise.all = function (values) {
    return new PromiseArray(values, undefined, true)._promise;
  };
  Promise.some = function (values, count) {
    return new PromiseArray(values, count, false)._promise;
  };
  Promise.any = function (values) {
    return new PromiseArray(values, 1, false)._promise;
  };
})();
複製程式碼

參考

Bluebird

相關文章