老生常談:Promise 用法與原始碼分析

?holyZhengs發表於2018-10-18

此文章是幾個月前寫得,發現沒有發表過,就在此發表一下。

背景

Promise本身是一個非同步程式設計的方案,讓處理過程變得更簡單。es6引入promise特性來處理JavaScript中的非同步場景。以前,處理非同步最常用的方法就是回撥函式,但是當過程稍微複雜一點,多個非同步操作集中在一起的時候,就容易出現一個回撥金字塔的情況,可讀性和可維護性都非常差,比如:

setTimeout(function () {
  console.log('ping');
  setTimeout(function () {
    console.log('pong');
    setTimeout(function () {
      console.log('end!');
    }, 1000);
  }, 1000);
}, 1000);
複製程式碼

Promise可以避免這種情況發生,將回撥巢狀轉變為鏈式呼叫,避免回撥金字塔的出現。

Promise基本用法

Promise有4種狀態:

  • fulfilled——成功狀態
  • rejected——失敗狀態
  • pending——執行狀態(未成功也未失敗)
  • settled——完成狀態
let promise = new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});
複製程式碼

可以通過then方法來處理返回的結果

// promise.then(onResolve, onReject)

promise.then(response => {
  console.log(response);
}, error => {
  console.log(error);
});
複製程式碼

then方法不僅僅是處理結果,而且還可以繼續返回promise物件

promise.then(response => {
  console.log(response); // success
  return 'another success';
}).then(response => {
  console.log(response); // another success 
});
複製程式碼

對reject狀態返回的結果的處理,可以通過then的第二個引數,也可以通過catch方法

promise.then(
  null,
  error => {
    console.log(error); // failure
  }
);
// 或
promise.catch(err => {
  console.log(err); // failure
});
複製程式碼

同時處理多個promise,不關注執行順序可以用all方法

let doSmth = new Promise(resolve => {
  resolve('doSmth');
}),
doSmthElse = new Promise(resolve => {
  resolve('doSmthElse');
}),
oneMore = new Promise(resolve => {
  resolve('oneMore'); 
});
Promise.all([
  doSmth,
  doSmthElse,
  oneMore
])
.then(response => {
  let [one, two, three] = response;
  console.log(one, two, three); // doSmth doSmthElse oneMore
});
複製程式碼

Promise.all()接收一個promises陣列,當全部fulfiled時,返回一個按順序的陣列 當其中一個reject時,返回第一個rejected的值 或者race方法,接收多個promise例項,組成一個新的promise,有一個變化的時候,外層promise跟著變化。 快捷方法:

  • Promise.resolve(value) 返回一個resolve(value)的promise 或直接返回這個value如果value本身時promise物件的話。
  • Promise.reject(value) 返回一個rejected狀態的promise,並且reject(value)

——參考ES6 Promise

原理

為了學習promise內部原理,最好是看其實現原始碼,then/promise是github上一個遵循promise A+規範的庫,其核心程式碼在core檔案中。那麼就從這個庫來學習。

function noop() {} // 定義一個空函式用於對比和例項化空promise,後面會用到

// States:
//  庫定義的4種狀態
// 0 - pending
// 1 - fulfilled with _value
// 2 - rejected with _value
// 3 - adopted the state of another promise, _value

var LAST_ERROR = null;  
var IS_ERROR = {};   // 這兩個用來捕獲錯誤
// 獲取obj中的then方法
function getThen(obj) {
  try {
    return obj.then;
  } catch (ex) {
    LAST_ERROR = ex;
    return IS_ERROR;
  }
}
// 當then中只傳進了一個回撥函式時呼叫此方法
function tryCallOne(fn, a) {
  try {
    return fn(a);
  } catch (ex) {
    LAST_ERROR = ex;
    return IS_ERROR;
  }
}
// 當then中傳入了兩個回撥函式時呼叫此方法
function tryCallTwo(fn, a, b) {
  try {
    fn(a, b);
  } catch (ex) {
    LAST_ERROR = ex;
    return IS_ERROR;
  }
}
複製程式碼
// Promise建構函式
function Promise(fn) {

// 檢驗是否例項化了promise物件,不能直接使用promise建構函式來封裝自己的程式碼
  if (typeof this !== 'object') {
    throw new TypeError('Promises must be constructed via new');
  }

// 檢驗傳進來的是否為函式,promise必須接受一個函式來進行例項化
  if (typeof fn !== 'function') {
    throw new TypeError('Promise constructor\'s argument is not a function');
  }

  this._deferredState = 0;  
// 與後面的this._deferreds關係密切,當resolve方法接收的是一個promise時,回用到他們

  this._state = 0;  // 對應上方4種狀態
  this._value = null; // 存放最終結果
  this._deferreds = null;  // 存放then中接收的處理函式
  if (fn === noop) return;  // 如果promise接收的是空函式,直接返回,結束。
  doResolve(fn, this);
}
複製程式碼

可以看到,我通過Promise建構函式例項化一個promise物件,在對引數進行檢查後,我們會執行doResolve(fn, this)方法,順藤摸瓜看看doResolve函式做了什麼

function doResolve(fn, promise) {
  var done = false; // 確保onFulfilled 和 onRejected只被呼叫一次
  var res = tryCallTwo(fn, function (value) {
    if (done) return;
    done = true;
    resolve(promise, value);
  }, function (reason) {
    if (done) return;
    done = true;
    reject(promise, reason);
  });
  if (!done && res === IS_ERROR) {
    done = true;
    reject(promise, LAST_ERROR);
  }
}
複製程式碼

這裡就是將兩個回撥函式分別傳給 fn 的 兩個引數,並確保他們只執行一次。 接下來就要看它的resolve方法。

function resolve(self, newValue) {
  if (newValue === self) {
    return reject(
      self,
      new TypeError('A promise cannot be resolved with itself.')
    );
  }
  if (
    newValue &&
    (typeof newValue === 'object' || typeof newValue === 'function')
  ) {
    var then = getThen(newValue);
    if (then === IS_ERROR) {
      return reject(self, LAST_ERROR);
    }
    if (
      then === self.then &&
      newValue instanceof Promise
    ) {
// 當接收的引數為promise,或thenable物件時。
      self._state = 3;
      self._value = newValue;
      finale(self); // 執行_deferreds 中的方法,如果有的話。
      return;
    } else if (typeof then === 'function') {
      doResolve(then.bind(newValue), self);
      return;
    }
  }
  self._state = 1;
  self._value = newValue;
  finale(self);
}
複製程式碼

resolve除了一些判斷外,就是根據接收到的引數的型別來修改state的值。如果接收到promise物件或thenable物件,state轉為3,並使用它的結果,如果時其他如字串型別等,state轉為1,直接使用該值。 有了結果之後,就要看一下then方法了。

Promise.prototype.then = function(onFulfilled, onRejected) {
  if (this.constructor !== Promise) {
    return safeThen(this, onFulfilled, onRejected);
  }
  var res = new Promise(noop);
  handle(this, new Handler(onFulfilled, onRejected, res));
  return res;
};
function safeThen(self, onFulfilled, onRejected) {
  return new self.constructor(function (resolve, reject) {
    var res = new Promise(noop);
    res.then(resolve, reject);
    handle(self, new Handler(onFulfilled, onRejected, res));
  });
}
複製程式碼

then方法也很簡單,就是用Handler包裝一個物件

function Handler(onFulfilled, onRejected, promise){
  this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}
複製程式碼

然後呼叫handle方法。整個過程就是建立一個新的promise,呼叫handle方法,將新的promise返回,以便實現鏈式呼叫。 下面看一下handle方法。

function handle(self, deferred) {
// self 移動指向最新的promise
  while (self._state === 3) {
    self = self._value;
  }
  if (Promise._onHandle) {
    Promise._onHandle(self);
  }
  if (self._state === 0) {
// 向_deferredState中新增handler處理過得物件,也就是{onFulfilled,onRejected,promise}

    if (self._deferredState === 0) {
      self._deferredState = 1;
      self._deferreds = deferred;
      return;
    }
    if (self._deferredState === 1) {
      self._deferredState = 2;
      self._deferreds = [self._deferreds, deferred];
      return;
    }
    self._deferreds.push(deferred);
    return;
  }
  handleResolved(self, deferred);
}
複製程式碼

handle方法就是根據state的值和_deferredState ,來決定要做的事情,我們來捋一捋,當我們的resolve方執行,state轉為1時,我們會進入then方法,然後進入handle方法,因為state為1,可以看到我們會直接進入handleResolved方法。

resolve   ->  then  ->  handle  -> handleResolved
複製程式碼

看看handleResolved函式是做什麼的

function handleResolved(self, deferred) {
  asap(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    if (cb === null) {
      if (self._state === 1) {
        resolve(deferred.promise, self._value);
      } else {
        reject(deferred.promise, self._value);
      }
      return;
    }
    var ret = tryCallOne(cb, self._value);
    if (ret === IS_ERROR) {
      reject(deferred.promise, LAST_ERROR);
    } else {
// 此處主要服務於promise的鏈式呼叫,因為promise通過返回一個新的promise來實現鏈式呼叫。
// 新的promise儲存在deferred.promise中
      resolve(deferred.promise, ret);
    }
  });
}
複製程式碼

過濾掉新增判斷,handleResolved就是使用結果值self._value呼叫then中的相應回撥(成功或失敗)。 那當resolve接收的是普通值得時候整個執行過程就知道了。

resolve  ->  then  ->  handle  -> handleResolved -> 執行onFulfilled或onRejected
複製程式碼

當我們resolve接收到得是一個promise或thenable物件時,我們進入到handle後,會進入while迴圈,直到self指向接收到的promise,以接收到的promise的結果為標準,在接收到的promise的 state===0 階段我們會將原始promise中拿到得onFulfilled以及onRejected回撥方法(包含在deferred物件中),新增到接收到的promise的 _deferreds 中,然後return。 存在 _deferreds 中的回撥在什麼時候執行呢? 我們可以看到無論時resolve還是reject,只要狀態改變都會執行 finale 方法,我們看一下 finale

function finale(self) {
  if (self._deferredState === 1) {
    handle(self, self._deferreds);
    self._deferreds = null;
  }
  if (self._deferredState === 2) {
    for (var i = 0; i < self._deferreds.length; i++) {
      handle(self, self._deferreds[i]);
    }
    self._deferreds = null;
  }
}
複製程式碼

因為每次執行此方法都是在state狀態改變的時候,所以進入handle函式後會直接進入handleResolved方法,然後使用self._value的結果值執行對應的回撥函式(onFulfilled 或 onRejected)。 最後看看reject

function reject(self, newValue) {
  self._state = 2;
  self._value = newValue;
  if (Promise._onReject) {
    Promise._onReject(self, newValue);
  }
  finale(self);
}
複製程式碼

這下清晰多了,再來捋一捋,

promise

總結

  • Promise本身是一個非同步程式設計的方案,讓處理過程變得更簡單。es6引入promise特性來處理JavaScript中的非同步場景,代替了傳統基於回撥的方案,防止瞭如回撥金字塔等現象的發生。
  • promise內部執行機制:使用promise封裝非同步函式,通過resolve和reject方法來處理結果,
    • 當發生錯誤時,reject會將state轉為狀態2(rejected)並呼叫對應的onRejected回撥函式,
    • 當成功時,resolve接收對應的結果,當結果時普通值(比如string型別)他會將state轉為狀態1,直接使用該值呼叫對應的onFulfilled回撥函式,
    • 當接收到的是一個promise物件或者thenable物件時,會將thenable物件轉為promise物件,並將當前state轉為3,將我們的onFulfilled和onRejected回掉函式儲存到接收到的promise中,並採用接收到的promise的結果為最終標準,當它的state發生變化時,執行相應的回撥函式。
  • 其鏈式呼叫時通過返回一個新的promise空物件來實現的,在當前的onFulfilled或onRejected回撥執行後,會將執行結果以及新的promise作為引數去呼叫onFulfilled或onRejected方法,實現值在鏈式中的傳遞。

相關文章