老生常談:Promise 用法與原始碼分析
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內部原理,最好是看其實現原始碼, ofollow,noindex">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本身是一個非同步程式設計的方案,讓處理過程變得更簡單。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方法,實現值在鏈式中的傳遞。