1. 程式人生 > >實現一個Promise-polyfill

實現一個Promise-polyfill

Promise

Promise是ES6標準提供的一個非同步操作的很好的語法糖,對於原本的回撥函式模式進行了封裝,實現了對於非同步操作的鏈式呼叫。並且配上generator以及async語法糖來使用更加方便。

雖然Promise當前在很多瀏覽器上都已經得到了支援,但是在看Promise的時候,發現對於Promise的很多地方仍然不是很瞭解。包括其內部的實現機制,寫這個程式碼的目的也是在於對Promise的使用更加了如指掌。

Promise的具體使用方法可以看我的這一篇部落格,這裡就不對Promise物件本身的使用進行說明了,預設大家都已經掌握基本的Promise的使用方法了。如果不甚瞭解的話,請看Promise、generator和async/await。

具體的程式碼可以參見我的github中的fake-promise

Promise物件的實現思路

初始化

首先,對於ES6原生的Promise物件來說,在初始化的過程中,我們傳遞的是一個function(resolve, reject){}函式作為引數,而這個函式是用來進行非同步操作的。

當寫在這個引數函式中的非同步操作成功之後,規範是將非同步操作得到的結果進行處理之後,呼叫resolve(data)函式來對結果進行鏈式處理。

如果非同步操作失敗的話,那麼自然就是將失敗原因處理之後,呼叫reject(err)函式。

如下:

    var p = new Promise(function
(resolve, reject) {
fs.readFile('./readme', function(err, data) { if (err) { reject(err); } else { resolve(data); } }); });

也就是這個兩個引數函式無論如何,都是會在非同步操作完成之後呼叫的。

那針對這一點,可以先這樣寫Promise的建構函式(這是Promise-polyfill的大體框架和初始化函式):

    const promiseStatusSymbol = Symbol('PromiseStatus'
); const promiseValueSymbol = Symbol('PromiseValue'); const STATUS = { PENDING: 'PENDING', FULFILLED: 'FULFILLED', REJECTED: 'REJECTED' }; const transition = function(status) { var self = this; return function (value) { this[promiseStatusSymbol] = status; this[promiseValueSymbol] = value; } } const FPromise = function(resolver) { if (typeof resolver !== 'function') { throw new TypeError('parameter 1 must be a function'); } this[promiseStatusSymbol] = STATUS.PENDING; this[promiseValueSymbol] = []; this.deps = {}; resolver( // 這裡返回兩個函式,這兩個函式也就是resolver和reject。 // 這兩個函式會分別對於當前Promise的狀態和值進行修改 transition.call(this, STATUS.FULFILLED), transition.call(this, STATUS.REJECTED) ); }

根據使用Promise的經驗,其整個生命週期應該是具有狀態的,當開始非同步操作,但是還沒有結果的時候,應該是掛起狀態PENDING,然後是成功和失敗的狀態。

傳入到建構函式中的函式需要在建構函式中被呼叫,來開始非同步操作。然後通過我們傳遞進去的兩個函式來分別修改成功和失敗的狀態以及值。

then方法

then方法應該是Promise進行鏈式呼叫的根本。

首先,then方法具有兩個引數,分別是成功和失敗的回撥,

然後,其應該返回一個新的Promise物件來給鏈式的下一個節點進行呼叫,

最後,這裡如果本身Promise物件的狀態已經是FULFILLED或者REJECTED了,那麼就可以直接呼叫回撥函數了,否則需要等待非同步操作的完成狀態發生。

    FPromise.prototype.then = function(onFulfilled, onRejected) {
      const self = this;
      return FPromise(function(resolve, reject) {
        const callback = function() {
          // 注意這裡,對於回撥函式執行時候的返回值,也需要儲存下來,
          // 因為鏈式呼叫的時候,這個引數應該傳遞給鏈式呼叫的下一個
          // resolve函式
          const resolveValue = onFulfilled(self[promiseValueSymbol]);
          resolve(resolveValue);
        }
        const errCallback = function() {
          const rejectValue = onRejected(self[promiseValueSymbol]);
          reject(rejectValue);
        }
        // 這裡是對當前Promise狀態的處理,如果上一個Promise在執行then方法之前就已經
        // 完成了,那麼下一個Promise對應的回撥應該直接執行
        if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
          return callback();
        } else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
          return errCallback();
        } else if (self[promiseStatusSymbol] === STATUS.PENDING) {
          self.deps.resolver = callback;
          self.deps.rejecter = errCallback;
        }
      })
    }

但是如果當前的狀態是PENDING呢,也就是之前Promise的非同步操作還沒有決議。這時候,成功和失敗的函式呼叫應該儲存到之前Promise的依賴當中。

當之前的Promise產生了一個決議,那麼就可以呼叫callbackerrCallback函數了。

這樣說可能不是很清晰,下面的圖可以看到Promise進行鏈式呼叫的方法。

fake-promise

上面的程式碼並不能夠實現當p1決議了之後,可以呼叫p1.then生成的回撥函式,所以這裡要對之前的程式碼進行一些修改。

const transition = function(status) {
  return (value) => {
    this[promiseValueSymbol] = value;
    setStatus.call(this, status);
  }
}
/** 
  * 對於狀態的改變進行控制,類似於存取器的效果。
  * 如果狀態從 PENDING --> FULFILLED,則呼叫鏈式的下一個onFulfilled函式
  * 如果狀態從 PENDING --> REJECTED, 則呼叫鏈式的下一個onRejected函式
  *
  * @returns void
  */
const setStatus = function(status) {
  this[promiseStatusSymbol] = status;
  if (status === STATUS.FULFILLED) {
    this.deps.resolver && this.deps.resolver();
  } else if (status === STATUS.REJECTED) {
    this.deps.rejecter && this.deps.rejecter();
  }
}

通過修改狀態時候的一個存取器來攔截狀態改變的操作,然後對於依賴的內容進行呼叫。

由於p2接收了p1返回的一個新的Promise物件,那麼p2也可以進行類似p1的操作,可以呼叫p1的then方法來繼續進行鏈式呼叫,比如我們定義了一個p3,接收p2鏈式呼叫的結果。

    var p3 = p2.then(function(data) {
      console.log(data);
    }, function(err) {
      console.error(err);
    })

而這裡p2的then方法中的兩個回撥函式又會被構造成callbackerrCallback函式,新增到p2的deps依賴物件當中。

這裡當然也是存在一點問題的,就比如上面p1的then方法返回了一個thenable物件,那麼就需要對這個物件進行處理,將其進行執行。

那麼如何執行呢,對於一個thenable物件來說,可以將其看成一個新的Promise,然後,這個Promise應該也是具有then方法的,直接呼叫其then方法其實就可以了,這裡很難說清楚,程式碼中的註釋有一定的解釋。

    const callback = function() {
      const resolveValue = onFulfilled(self[promiseValueSymbol]);
      // 這裡是對於當返回值是一個thenable物件的時候,
      // 需要對其進行特殊處理,直接呼叫它的then方法來
      // 獲取一個返回值
      if (resolveValue && typeof resolveValue.then === 'function') {
        // 這裡呼叫了resolve之後,也就是將這個內嵌的Promise
        // 得到的值繫結到當前Promise的依賴中,其實和下面的
        // unthenable的情況是一致的
        resolveValue.then(function(data) {
          resolve(data);
        }, function(err) {
          reject(err);
        });
      } else {
        // 注意,這裡是then方法進行鏈式呼叫的連線點
        // 當初始化狀態或者上一次Promise的狀態發生改變的時候
        // 這裡會通過呼叫當前Promise成功的方法,來進行當前Promise的狀態改變
        // 以及呼叫鏈式的下個Promise的回撥
        resolve(resolveValue);
      }
    }

這樣就基本上實現了一個簡單的Promise的功能,雖然程式碼很短,但是邏輯非常複雜,一不小心就會繞進去。

結論

具體的程式碼可以參見我的github中的fake-promise

這個依賴收集的方法在很多專案的原始碼中都有使用過,比如Vue、RequireJS等,RequireJS的實現也可以看這裡

這裡有幾個重點,其實在呼叫then方法的時候,只是依賴新增的過程,並不是執行兩個回撥函式。

並且在初始化的時候,傳入的函式引數會立即呼叫,而resolve和reject兩個引數是用來對於狀態進行修改的。

Promise的鏈式呼叫的過程主要是一個對於依賴進行依次收集的過程,這個過程將每個Promise耦合在了一起。