用過 Promise,但是總是有點似懂非懂的感覺,也看過很多文章,還是搞不懂 Promise的 實現原理,后面自己邊看文章,邊調試代碼,終於慢慢的有感覺了,下面就按自己的理解來實現一個 Promise。
已將每一步的代碼都放在了 github 上,方便大家閱讀。如果覺得好的話,歡迎star。
想要完全理解代碼,需要理解 this 和閉包的含義。
Promise是什么
簡單來說,Promise 主要就是為了解決異步回調的問題。用 Promise 來處理異步回調使得代碼層次清晰,便於理解,且更加容易維護。其主流規范目前主要是 Promises/A+ 。對於 Promise 用法不熟悉的,可以參看我的這篇文章——es6學習筆記5--promise,理解了再來看這篇文章,會對你有很大幫助的。
在開始前,我們先寫一個 promise 應用場景來體會下 promise 的作用。目前谷歌和火狐已經支持 es6 的 promise。我們采用 setTimeout 來模擬異步的運行,具體代碼如下:
function fn1(resolve, reject) { setTimeout(function() { console.log('步驟一:執行'); resolve('1'); },500); } function fn2(resolve, reject) { setTimeout(function() { console.log('步驟二:執行'); resolve('2'); },100); } new Promise(fn1).then(function(val){ console.log(val); return new Promise(fn2); }).then(function(val){ console.log(val); return 33; }).then(function(val){ console.log(val); });
最終我們寫的promise同樣可以實現這個功能。
初步構建
下面我們來寫一個簡單的 promsie。Promise 的參數是函數 fn,把內部定義 resolve 方法作為參數傳到 fn 中,調用 fn。當異步操作成功后會調用 resolve 方法,然后就會執行 then 中注冊的回調函數。
function Promise(fn){ //需要一個成功時的回調 var callback; //一個實例的方法,用來注冊異步事件 this.then = function(done){ callback = done; } function resolve(){ callback(); } fn(resolve); }
加入鏈式支持
下面加入鏈式,成功回調的方法就得變成數組才能存儲。同時我們給 resolve 方法添加參數,這樣就不會輸出 undefined。
function Promise(fn) { var promise = this, value = null; promise._resolves = []; this.then = function (onFulfilled) { promise._resolves.push(onFulfilled); return this; }; function resolve(value) { promise._resolves.forEach(function (callback) { callback(value); }); } fn(resolve); }
-
promise = this, 這樣我們不用擔心某個時刻 this 指向突然改變問題。
-
調用 then 方法,將回調放入 promise._resolves 隊列;
-
創建 Promise 對象同時,調用其 fn, 並傳入 resolve 方法,當 fn 的異步操作執行成功后,就會調用 resolve ,也就是執行
promise._resolves
隊列中的回調; -
resolve 方法
接收一個參數,即異步操作返回的結果,方便傳值。 - then方法中的 return this 實現了鏈式調用。
但是,目前的 Promise 還存在一些問題,如果我傳入的是一個不包含異步操作的函數,resolve就會先於 then 執行,也就是說 promise._resolves 是一個空數組。
為了解決這個問題,我們可以在 resolve 中添加 setTimeout,來將 resolve
中執行回調的邏輯放置到 JS 任務隊列末尾。
function resolve(value) { setTimeout(function() { promise._resolves.forEach(function (callback) { callback(value); }); },0); }
引入狀態
剖析 Promise 之基礎篇 說 這里存在一點問題: 如果 Promise 異步操作已經成功,之后調用 then 注冊的回調再也不會執行了,而這是不符合我們預期的。
對於這句話不是很理解,有知道的可以留言說下,最好能給實例說明下。但我個人覺得是,then 中的注冊的回調都會在 resolve 運行之前就添加到數組當中,不會存在不執行的情況啊。
接着上面的步伐,引入狀態:
function Promise(fn) { var promise = this, value = null; promise._resolves = []; promise._status = 'PENDING'; this.then = function (onFulfilled) { if (promise._status === 'PENDING') { promise._resolves.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(value) { setTimeout(function(){ promise._status = "FULFILLED"; promise._resolves.forEach(function (callback) { callback(value); }) },0); } fn(resolve); }
每個 Promise 存在三個互斥狀態:pending、fulfilled、rejected。Promise 對象的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和從 pending 變為 rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對 Promise 對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
加上異步結果的傳遞
目前的寫法都沒有考慮異步返回的結果的傳遞,我們來加上結果的傳遞:
function resolve(value) { setTimeout(function(){ promise._status = "FULFILLED"; promise._resolves.forEach(function (callback) { value = callback(value); }) },0); }
串行 Promise
串行 Promise 是指在當前 promise 達到 fulfilled 狀態后,即開始進行下一個 promise(后鄰 promise)。例如我們先用ajax從后台獲取用戶的的數據,再根據該數據去獲取其他數據。
這里我們主要對 then 方法進行改造:
this.then = function (onFulfilled) { return new Promise(function(resolve) { function handle(value) { var ret = isFunction(onFulfilled) && onFulfilled(value) || value; resolve(ret); } if (promise._status === 'PENDING') { promise._resolves.push(handle); } else if(promise._status === FULFILLED){ handle(value); } }) };
then 方法該改變比較多啊,這里我解釋下:
-
注意的是,new Promise() 中匿名函數中的 promise (promise._resolves 中的 promise)指向的都是上一個 promise 對象, 而不是當前這個剛剛創建的。
-
首先我們返回的是新的一個promise對象,因為是同類型,所以鏈式仍然可以實現。
-
其次,我們添加了一個 handle 函數,handle 函數對上一個 promise 的 then 中回調進行了處理,並且調用了當前的 promise 中的 resolve 方法。
-
接着將 handle 函數添加到 上一個promise 的 promise._resolves 中,當異步操作成功后就會執行 handle 函數,這樣就可以 執行 當前 promise 對象的回調方法。我們的目的就達到了。
有些人在這里可能會有點犯暈,有必要對執行過程分析一下,具體參看以下代碼:
new Promise(fn1).then(fn2).then(fn3)})
fn1, fn2, fn3的函數具體可參看最前面的定義。
-
首先我們創建了一個 Promise 實例,這里叫做 promise1;接着會運行 fn1(resolve);
-
但是 fn1 中有一個 setTimeout 函數,於是就會先跳過這一部分,運行后面的第一個 then 方法;
-
then 返回一個新的對象 promise2, promise2 對象的 resolve 方法和 then 方法的中回調函數 fn2 都被封裝在 handle 中, 然后 handle 被添加到 promise1._resolves 數組中。
-
接着運行第二個 then 方法,同樣返回一個新的對象 promise3, 包含 promise3 的 resolve 方法和 回調函數 fn3 的 handle 方法被添加到 promise2._resolves 數組中。
-
到此兩個 then 運行結束。 setTimeout 中的延遲時間一到,就會調用 promise1的 resolve方法。
-
resolve 方法的執行,會調用 promise1._resolves 數組中的回調,之前我們添加的 handle 方法就會被執行; 也就是 fn2 和 promsie2 的 resolve 方法,都被調用了。
-
以此類推,fn3 會和 promise3 的 resolve 方法 一起執行,因為后面沒有 then 方法了,promise3._resolves 數組是空的 。
-
至此所有回調執行結束
但這里還存在一個問題,就是我們的 then 里面函數不能對 Promise 對象進行處理。這里我們需要再次對 then 進行修改,使其能夠處理 promise 對象。
this.then = function (onFulfilled) { return new Promise(function(resolve) { function handle(value) { var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value; if( ret && typeof ret ['then'] == 'function'){ ret.then(function(value){ resolve(value); }); } else { resolve(ret); } } if (promise._status === 'PENDING') { promise._resolves.push(handle); } else if(promise._status === FULFILLED){ handle(value); } }) };
在 then 方法里面,我們對 ret 進行了判斷,如果是一個 promise 對象,就會調用其 then 方法,形成一個嵌套,直到其不是promise對象為止。同時 在 then 方法中我們添加了調用 resolve 方法,這樣鏈式得以維持。
失敗處理
異步操作不可能都成功,在異步操作失敗時,標記其狀態為 rejected,並執行注冊的失敗回調。
有了之前處理 fulfilled 狀態的經驗,支持錯誤處理變得很容易。毫無疑問的是,在注冊回調、處理狀態變更上都要加入新的邏輯:
this.then = function (onFulfilled, onRejected) { return new Promise(function(resolve, reject) { function handle(value) { ....... } function errback(reason){ reason = isFunction(onRejected) && onRejected(reason) || reason; reject(reason); } if (promise._status === 'PENDING') { promise._resolves.push(handle); promise._rejects.push(errback); } else if(promise._status === 'FULFILLED'){ handle(value); } else if(promise._status === 'REJECTED') { errback(promise._reason); } }) }; function reject(value) { setTimeout(function(){ promise._status = "REJECTED"; promise._rejects.forEach(function (callback) { promise._reason = callback( value); }) },0); }
添加Promise.all方法
Promise.all 可以接收一個元素為 Promise 對象的數組作為參數,當這個數組里面所有的 Promise 對象都變為 resolve 時,該方法才會返回。
具體代碼如下:
Promise.all = function(promises){ if (!Array.isArray(promises)) { throw new TypeError('You must pass an array to all.'); }
// 返回一個promise 實例 return new Promise(function(resolve,reject){ var i = 0, result = [], len = promises.length, count = len;
// 每一個 promise 執行成功后,就會調用一次 resolve 函數 function resolver(index) { return function(value) { resolveAll(index, value); }; } function rejecter(reason){ reject(reason); } function resolveAll(index,value){
// 存儲每一個promise的參數 result[index] = value;
// 等於0 表明所有的promise 都已經運行完成,執行resolve函數 if( --count == 0){ resolve(result) } } // 依次循環執行每個promise for (; i < len; i++) {
// 若有一個失敗,就執行rejecter函數 promises[i].then(resolver(i),rejecter); } }); }
Promise.all會返回一個 Promise 實例,該實例直到參數中的所有的 promise 都執行成功,才會執行成功回調,一個失敗就會執行失敗回調。
日常開發中經常會遇到這樣的需求,在不同的接口請求數據然后拼合成自己所需的數據,通常這些接口之間沒有關聯(例如不需要前一個接口的數據作為后一個接口的參數),這個時候 Promise.all 方法就可以派上用場了。
添加Promise.race方法
該函數和 Promise.all 相類似,它同樣接收一個數組,不同的是只要該數組中的任意一個 Promise 對象的狀態發生變化(無論是 resolve 還是 reject)該方法都會返回。我們只需要對 Promise.all 方法稍加修改就可以了。
Promise.race = function(promises){ if (!Array.isArray(promises)) { throw new TypeError('You must pass an array to race.'); } return Promise(function(resolve,reject){ var i = 0, len = promises.length; function resolver(value) { resolve(value); } function rejecter(reason){ reject(reason); } for (; i < len; i++) { promises[i].then(resolver,rejecter); } }); }
代碼中沒有類似一個 resolveAll 的函數,因為我們不需要等待所有的 promise 對象狀態都發生變化,只要一個就可以了。
添加其他API以及封裝函數
到這里,Promise 的主要API都已經完成了,另外我們在添加一些比較常見的方法。也對一些可能出現的錯誤進行了處理,最后對其進行封裝。
完整的代碼如下:

(function(window,undefined){ // resolve 和 reject 最終都會調用該函數 var final = function(status,value){ var promise = this, fn, st; if(promise._status !== 'PENDING') return; // 所以的執行都是異步調用,保證then是先執行的 setTimeout(function(){ promise._status = status; st = promise._status === 'FULFILLED' queue = promise[st ? '_resolves' : '_rejects']; while(fn = queue.shift()) { value = fn.call(promise, value) || value; } promise[st ? '_value' : '_reason'] = value; promise['_resolves'] = promise['_rejects'] = undefined; }); } //參數是一個函數,內部提供兩個函數作為該函數的參數,分別是resolve 和 reject var Promise = function(resolver){ if (!(typeof resolver === 'function' )) throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); //如果不是promise實例,就new一個 if(!(this instanceof Promise)) return new Promise(resolver); var promise = this; promise._value; promise._reason; promise._status = 'PENDING'; //存儲狀態 promise._resolves = []; promise._rejects = []; // var resolve = function(value) { //由於apply參數是數組 final.apply(promise,['FULFILLED'].concat([value])); } var reject = function(reason){ final.apply(promise,['REJECTED'].concat([reason])); } resolver(resolve,reject); } Promise.prototype.then = function(onFulfilled,onRejected){ var promise = this; // 每次返回一個promise,保證是可thenable的 return new Promise(function(resolve,reject){ function handle(value) { // 這一步很關鍵,只有這樣才可以將值傳遞給下一個resolve var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value; //判斷是不是promise 對象 if (ret && typeof ret ['then'] == 'function') { ret.then(function(value) { resolve(value); }, function(reason) { reject(reason); }); } else { resolve(ret); } } function errback(reason){ reason = typeof onRejected === 'function' && onRejected(reason) || reason; reject(reason); } if(promise._status === 'PENDING'){ promise._resolves.push(handle); promise._rejects.push(errback); }else if(promise._status === FULFILLED){ // 狀態改變后的then操作,立刻執行 callback(promise._value); }else if(promise._status === REJECTED){ errback(promise._reason); } }); } Promise.prototype.catch = function(onRejected){ return this.then(undefined, onRejected) } Promise.prototype.delay = function(ms,value){ return this.then(function(ori){ return Promise.delay(ms,value || ori); }) } Promise.delay = function(ms,value){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(value); console.log('1'); },ms); }) } Promise.resolve = function(arg){ return new Promise(function(resolve,reject){ resolve(arg) }) } Promise.reject = function(arg){ return Promise(function(resolve,reject){ reject(arg) }) } Promise.all = function(promises){ if (!Array.isArray(promises)) { throw new TypeError('You must pass an array to all.'); } return Promise(function(resolve,reject){ var i = 0, result = [], len = promises.length, count = len //這里與race中的函數相比,多了一層嵌套,要傳入index function resolver(index) { return function(value) { resolveAll(index, value); }; } function rejecter(reason){ reject(reason); } function resolveAll(index,value){ result[index] = value; if( --count == 0){ resolve(result) } } for (; i < len; i++) { promises[i].then(resolver(i),rejecter); } }); } Promise.race = function(promises){ if (!Array.isArray(promises)) { throw new TypeError('You must pass an array to race.'); } return Promise(function(resolve,reject){ var i = 0, len = promises.length; function resolver(value) { resolve(value); } function rejecter(reason){ reject(reason); } for (; i < len; i++) { promises[i].then(resolver,rejecter); } }); } window.Promise = Promise; })(window);
下載完整版代碼,點擊 github ,如果覺得好的話,歡迎star。
代碼寫完了,總要寫幾個實例看看效果啊,具體看下面的測試代碼:
var getData100 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve('100ms'); },1000); }); } var getData200 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve('200ms'); },2000); }); } var getData300 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ reject('reject'); },3000); }); } getData100().then(function(data){ console.log(data); // 100ms return getData200(); }).then(function(data){ console.log(data); // 200ms return getData300(); }).then(function(data){ console.log(data); }, function(data){ console.log(data); // 'reject' }); Promise.all([getData100(), getData200()]).then(function(data){ console.log(data); // [ "100ms", "200ms" ] }); Promise.race([getData100(), getData200(), getData300()]).then(function(data){ console.log(data); // 100ms }); Promise.resolve('resolve').then(function(data){ console.log(data); //'resolve' }) Promise.reject('reject函數').then(function(data){ console.log(data); }, function(data){ console.log(data); //'reject函數' })