從使用角度漸進式剖析Promise源碼

開篇

最近在 github 上看到了一個 extremely lightweight Promise polyfill 實現,打開源碼發現只有240行,果真極其輕量級,因而帶着驚歎和好奇的心理去了解了下其具體實現。
源碼的 github 地址:promise-polyfill前端

Promise 對於前端來講,是個老生常談的話題,Promise 的出現解決了 js 回調地域的問題。目前市面上有不少 Promise 庫,但其最終實現都要聽從 Promise/A+ 規範,這裏對規範不作解讀,有興趣的能夠查看連接內容。
Promise/A+規範連接
Promise/A+規範中文連接node

本篇文章將從 Promise 的使用角度來剖析源碼具體實現。git

API 列表

Promise  // 構造函數
Promise.prototype.then
Promise.prototype.catch
Promise.prototype.finally

// 靜態方法
Promise.resolve
Promise.reject
Promise.race
Promise.all

源碼解析

構造函數

使用
Promise 使用第一步,構造實例,傳入 Function 形參,形參接收兩個 Function 類型參數resolve, rejectgithub

const asyncTask = () => {};
const pro = new Promise((resolve, reject) => {
  asyncTask((err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
});

源碼c#

function Promise(fn) {
  if (!(this instanceof Promise))
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function') throw new TypeError('not a function');
  this._state = 0;
  this._handled = false;
  this._value = undefined;
  this._deferreds = [];
  doResolve(fn, this);
}

function doResolve(fn, self) {
  // done變量保護 resolve 和 reject 只執行一次
  // 這個done在 Promise.race()函數中有用
  var done = false;
  try {
    // 當即執行 Promise 傳入的 fn(resolve,reject)
    fn(
      function(value) {
        // resolve 回調
        if (done) return;
        done = true;
        resolve(self, value);
      },
      function(reason) {
        // reject 回調
        if (done) return;
        done = true;
        reject(self, reason);
      }
    );
  } catch (ex) {
    if (done) return;
    done = true;
    reject(self, ex);
  }
}

Promise必須經過構造函數實例化來使用,傳入 Promise 構造函數的形參 fn 在doResolve方法內是 當即調用執行 的,並無異步(指放入事件循環隊列)處理。doResolve內部針對 fn 函數的回調參數作了封裝處理,done變量保證了 resolve reject 方法只執行一次,這在後面說到的Promise.race()函數實現有很大用處。segmentfault

Promise 實例的內部變量介紹

名稱 類型 默認值 描述
_state Number 0 Promise內部狀態碼
_handled Boolean false onFulfilled,onRejected是否被處理過
_value Any undefined Promise 內部值,resolve 或者 reject返回的值
_deferreds Array [] 存放 Handle 實例對象的數組,緩存 then 方法傳入的回調

_state枚舉值類型api

_state === 0  // pending
_state === 1  // fulfilled,執行了resolve函數,而且_value instanceof Promise === true
_state === 2  // rejected,執行了reject函數
_state === 3  // fulfilled,執行了resolve函數,而且_value instanceof Promise === false

注意:這裏_state區分了1 和 3 兩種狀態,下面會解釋緣由數組

/**
 * Handle 構造函數
 * @param onFulfilled resolve 回調函數
 * @param onRejected reject 回調函數
 * @param promise 下一個 promise 實例對象
 * @constructor
 */
function Handler(onFulfilled, onRejected, promise) {
  this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}

_deferreds數組的意義:當在 Promise 內部調用了異步處理任務時,pro.then(onFulfilled,onRejected)傳入的兩個函數不會當即執行,因此此時會把當前的回調和下一個 pro 對象關聯緩存起來,待到 resolve 或者 reject觸發調用時,會去 forEach 這個_deferreds數組中的每一個 Handle 實例去處理對應的 onFulfilled,onRejected 方法。promise

Promise 內部 resolve reject finale 方法

上面說到,doResolve 內部作了 fn 的當即執行,並保證 resolve 和 reject 方法只執行一次,接下來講說resolve 和 reject 內部具體作了什麼緩存

function resolve(self, newValue) {
  try {
    // resolve 的值不能爲自己 this 對象
    // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
    if (newValue === self)
      throw new TypeError('A promise cannot be resolved with itself.');
    // 針對 resolve 值爲 Promise 對象的狀況處理
    if (
      newValue &&
      (typeof newValue === 'object' || typeof newValue === 'function')
    ) {
      var then = newValue.then;
      if (newValue instanceof Promise) {
        self._state = 3;
        self._value = newValue;
        finale(self);
        return;
      } else if (typeof then === 'function') {
        // 兼容類 Promise 對象的處理方式,對其 then 方法繼續執行 doResolve
        doResolve(bind(then, newValue), self);
        return;
      }
    }
    //  resolve 正常值的流程,_state = 1
    self._state = 1;
    self._value = newValue;
    finale(self);
  } catch (e) {
    reject(self, e);
  }
}

function reject(self, newValue) {
  self._state = 2;
  self._value = newValue;
  finale(self);
}

function finale(self) {
  //  Promise reject 狀況,可是 then 方法未提供 reject 回調函數參數 或者 未實現 catch 函數
  if (self._state === 2 && self._deferreds.length === 0) {
    Promise._immediateFn(function() {
      if (!self._handled) {
        Promise._unhandledRejectionFn(self._value);
      }
    });
  }

  for (var i = 0, len = self._deferreds.length; i < len; i++) {
    // 這裏調用以前 then 方法傳入的onFulfilled, onRejected函數
    // self._deferreds[i] => Handler 實例對象
    handle(self, self._deferreds[i]);
  }
  self._deferreds = null;
}

resolve,reject 是由用戶在異步任務裏面觸發的回調函數
調用 resolve reject 方法的注意點
一、newValue不能爲當前的 this 對象,即下面的這樣寫法是錯誤的

const pro = new Promise((resolve)=>{setTimeout(function () {
  resolve(pro);
},1000)});
pro.then(data => console.log(data)).catch(err => {console.log(err)});

由於resolve作了 try catch 的操做,直接會進入 reject 流程。

二、newValue能夠爲另外一個Promise 對象類型實例, resolve 的值返回的是另外一個 Promise 對象實例的內部的_value,而不是其自己 Promise 對象。便可以這樣寫

const pro1 = new Promise((resolve)=>{setTimeout(function () {
  resolve(100);
},2000)});
const pro = new Promise((resolve)=>{setTimeout(function () {
  resolve(pro1);
},1000)});
pro.then(data => console.log('resolve' + data)).catch(err => {console.log('reject' + err)});
// 輸出結果:resolve 100
// data 並非pro1對象

具體緣由就在 resolve 方法體內部作了newValue instanceof Promise的判斷,並將當前的_state=3,self._value = newValue,而後進入 finale 方法體,在 handle 方法作了核心處理,這個下面介紹 handle 方法會說到;

這裏有一個注意點,resolve 的 value 多是其餘框架的 Promise(好比:global.Promise,nodejs 內部的 Promise 實現) 構造實例,因此在typeof then === 'function'條件下作了doResolve(bind(then, newValue), self);的從新調用,繼續執行當前類型的 Promise then 方法,即又從新回到了doResolve流程。

若是這裏的實現方式稍微調整下,即無論newValue是自身的 Promise 實例仍是其餘框架實現的 Promise實例,都執行doResolve(bind(then, newValue), self)也能行得通,只不過會多執行 then 方式一次,從代碼性能上說,上面的實現方式會更好。參照代碼以下

function resolve(self, newValue) {
  try {
    // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
    if (newValue === self)
      throw new TypeError('A promise cannot be resolved with itself.');
    if (
      newValue &&
      (typeof newValue === 'object' || typeof newValue === 'function')
    ) {
      // 這裏簡單粗暴處理,不管是 Promise 仍是 global.Promise
      // 都直接調用doResolve
      var then = newValue.then;
      if (typeof then === 'function') {
        doResolve(bind(then, newValue), self);
        return;
      }
    }
    //  resolve 正常值的流程,_state = 1
    self._state = 1;
    self._value = newValue;
    finale(self);
  } catch (e) {
    reject(self, e);
  }
}

全部 resolve 和 reject 的值最終都會去到finale函數中去處理,只不過在這裏的_state狀態會有所不一樣;當Promise 出現reject的狀況時,而沒有提供 onRejected 函數時,內部會打印一個錯誤出來,提示要捕獲錯誤。代碼實現即

const pro = new Promise((resolve,reject)=>{setTimeout(function () {
  reject(100);
},1000)});
pro.then(data => console.log(data));  // 會報錯
pro.then(data => console.log(data)).catch();  // 會報錯
pro.then(data => console.log(data)).catch(()=>{});  // 不會報錯
pro.then(data => console.log(data),()=>{})  // 不會報錯

then、catch、finally 方法

第二步,調用 then 方法來處理回調,支持無限鏈式調用,then 方法第一個參數成功回調,第二個參數失敗或者異常回調

源碼

function noop() {}

Promise.prototype.then = function(onFulfilled, onRejected) {
  var prom = new this.constructor(noop);
  handle(this, new Handler(onFulfilled, onRejected, prom));
  return prom;
};

Promise.prototype['catch'] = function(onRejected) {
  return this.then(null, onRejected);
};

Promise.prototype['finally'] = function(callback) {
  var constructor = this.constructor;
  return this.then(
    function(value) {
      return constructor.resolve(callback()).then(function() {
        return value;
      });
    },
    function(reason) {
      return constructor.resolve(callback()).then(function() {
        return constructor.reject(reason);
      });
    }
  );
};

Promise.prototype.then方法內部構造了一個新的Promsie 實例並返回,這樣從 api 角度解決了 Promise 鏈式調用的問題,並且值得注意的是,每一個 then 方法返回的都是一個新的 Promise 對象,並非當前的 this連接調用方式。最終的處理都會調用 handle 方法。

catch方法在 then 方法上作了一個簡單的封裝,因此從這裏也能夠看出,then 方法的形參並非必傳的,catch 只接收onRejected。

finally方法不論是調用了 then 仍是 catch,最終都會執行到finally的 callback

核心邏輯:handle方法內部實現

上面說了這麼多,最終的 resolve reject 回調處理都會進入到 handle 方法中,來處理onFulfilled 和 onRejected,先看源碼

Promise._immediateFn =
  (typeof setImmediate === 'function' &&
    function(fn) {
      setImmediate(fn);
    }) ||
  function(fn) {
    setTimeoutFunc(fn, 0);
  };
  
function handle(self, deferred) {
  // 若是當前的self._value instanceof Promise
  // 將self._value => self,接下來處理新 Promise
  while (self._state === 3) {
    self = self._value;
  }
  // self._state=== 0 說明尚未執行 resolve || reject 方法
  // 此處將 handle 掛起
  if (self._state === 0) {
    self._deferreds.push(deferred);
    return;
  }
  self._handled = true;
  // 經過事件循環異步來作回調的處理
  Promise._immediateFn(function() {
    // deferred.promise :第一個 Promise then 方法 返回的新 Promise 對象
    // 這裏調用下一個 Promise 對象的 then 方法的回調函數
    // 若是當前 Promise resolve 了,則調用下一個 Promise 的 resolve方法,反之,則調用下一個 Promise 的 reject 回調
    // 若是當前 Promise resolve 了,則調用下一個 Promise 的 resolve方法
    // cb回調方法:若是本身有onFulfilled||onRejected方法,則執行本身的方法;若是沒有,則調用下一個 Promise 對象的onFulfilled||onRejected
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    // 本身沒有回調函數,進入下一個 Promise 對象的回調
    if (cb === null) {
      (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
      return;
    }
    // 本身有回調函數,進入本身的回調函數
    var ret;
    try {
      ret = cb(self._value);
    } catch (e) {
      reject(deferred.promise, e);
      return;
    }
    // 處理下一個 Promise 的 then 回調方法
    // ret 做爲上一個Promise then 回調 return的值 => 返回給下一個Promise then 做爲輸入值
    resolve(deferred.promise, ret);
  });
}

self._state === 3,說明當前 resolve(promise)方法回傳的值類型爲 Promise 對象,
即 self._value instanceOf Promise === true, 將 self=self._value,即當前處理變動到了新的 Promise 對象上 ,若是當前 promise對象內部狀態是fulfilled或者 rejected,則直接處理onFulfilled 或者 onRejected回調;若是仍然是 padding 狀態,則繼續等待。這就很好的解釋了爲何resolve(pro1),pro.then的回調取的值倒是 pro1._value.
從使用角度來看

const pro1 = new Promise(resolve=>{setTimeout(()=>{resolve(100)},1000)})  // 執行耗時1s 的異步任務
pro.then(()=>pro1).then(data => console.log(data)).catch(err => {});
// 輸出結果: 正常打印了100,data並非當前的pro1對象

pro1內部是耗時1s 的異步任務,此時self._state === 0,即內部是 Padding 狀態,則將deferred對象 push 到_deferreds數組裏面,而後等待 pro1內部調用resolve(100)時,繼續上面resolve方法體執行

const pro1 = new Promise(resolve=>resolve(100)}) // 執行同步任務
pro.then(()=>pro1).then(data => console.log(data)).catch(err => {});
// 輸出結果: 正常打印了100,data並非當前的pro1對象

可是若是pro1內部是同步任務,當即執行的話,當前的self._state === 1,即調過 push 到_deferreds數組的操做,執行最後的onFulfilled, onRejected回調,onFulfilled, onRejected會被放入到事件循環隊列裏面執行,即執行到了Promise._immediateFn

Promise._immediateFn回調函數放到了事件循環隊列裏面來執行
這裏的deferred對象存放了當前的onFulfilled和onRejected回調函數和下一個 promise 對象。
當前對象的onFulfilled和onRejected若是存在時,則執行本身的回調;

pro.then(data => data}).then(data => data).catch(err => {});
// 正確寫法: 輸出兩次  data

注意:then 方法必定要作 return 下一個值的操做,由於當前的 ret 值會被帶入到下一個 Promise 對象,即 resolve(deferred.promise, ret)。若是不提供返回值,則第二個 then 的 data 會變成 undefined,即這樣的錯誤寫法

pro.then(data => {}}).then(data => data).catch(err => {});
// 錯誤寫法: 第二個 then 方法的 data 爲 undefined

若是onFulfilled和onRejected回調不存在,則執行下一個 promise 的回調並攜帶當前的_value 值。便可以這樣寫

pro.then().then().then().then(data => {}).catch(err => {});
// 正確寫法: 第四個 then 方法仍然能取到第一個pro 的內部_value 值
// 固然前面的三個 then 寫起來毫無用處

因此針對下面的狀況:當第一個 then 提供了 reject 回調,後面又跟了個 catch 方法。
當 reject 時,會優先執行第一個 Promise 的onRejected回調函數,catch 是在下一個 Promise 對象上的捕獲錯誤方法

pro.then(data => data,err => err).catch(err => err);

最終總結:resolve 要麼提供帶返回值的回調,要麼不提供回調函數

靜態方法:race

Promise.race = function(values) {
  return new Promise(function(resolve, reject) {
    for (var i = 0, len = values.length; i < len; i++) {
      // 由於doResolve方法內部 done 變量控制了對 resolve reject 方法只執行一次的處理
      // 因此這裏實現很簡單,清晰明瞭,最快的 Promise 執行了  resolve||reject,後面相對慢的 // Promise都不執行
      values[i].then(resolve, reject);
    }
  });
};

用法

Promise.race([pro1,pro2,pro3]).then()

race的實現很是巧妙,對當前的 values(必須是 Promise 數組) for 循環執行每一個 Promise 的 then 方法,resolve, reject方法對於全部race中 promise 對象都是公用的,從而利用doResolve內部的 done變量,保證了最快執行的 Promise 能作 resolve reject 的回調,從而達到了多個Promise race 競賽的機制,誰跑的快執行誰。

靜態方法:all

Promise.all = function(arr) {
  return new Promise(function(resolve, reject) {
    if (!arr || typeof arr.length === 'undefined')
      throw new TypeError('Promise.all accepts an array');
    var args = Array.prototype.slice.call(arr);
    if (args.length === 0) return resolve([]);
    var remaining = args.length;

    function res(i, val) {
      try {
        // 若是 val 是 Promise 對象的話,則執行 Promise,直到 resolve 了一個非 Promise 對象
        if (val && (typeof val === 'object' || typeof val === 'function')) {
          var then = val.then;
          if (typeof then === 'function') {
            then.call(
              val,
              function(val) {
                res(i, val);
              },
              reject
            );
            return;
          }
        }
        // 用當前resolve||reject 的值重寫 args[i]{Promise} 對象
        args[i] = val;
        // 直到全部的 Promise 都執行完畢,則 resolve all 的 Promise 對象,返回args數組結果
        if (--remaining === 0) {
          resolve(args);
        }
      } catch (ex) {
        // 只要其中一個 Promise 出現異常,則所有的 Promise 執行退出,進入 catch異常處理
        // 由於 resolve 和 reject 回調有 done 變量的保證只能執行一次,因此其餘的 Promise 都不執行
        reject(ex);
      }
    }

    for (var i = 0; i < args.length; i++) {
      res(i, args[i]);
    }
  });
};

用法

Promise.all([pro1,pro2,pro3]).then()

all 等待全部的 Promise 都執行完畢,纔會執行 Promise.all().then()回調,只要其中一個出錯,則直接進入錯誤回調,由於對於全部 all 中 promise 對象 reject 回調是公用的,利用doResolve內部的 done變量,保證一次錯誤終止全部操做。

可是對於 resolve 則不同, resolve 回調函數經過 res 遞歸調用本身,從而保證其值_value不爲 Promise 類型才結束,並將_value 賦值到 args 數組,最後直到全部的數組Promise都處理完畢由統一的 resolve 方法結束當前的 all 操做,進入 then 處理流程。

結束語

本篇針對 Promise 的全部 api 作了詳細的代碼解釋和使用場景,篇幅可能過長,看起來比較費力,若是有寫的不對的地方歡迎指正。

最後附上個人 github 源碼註釋版連接 promise源碼註釋版

相關文章
相關標籤/搜索