如何實作JavaScript Promise?
本篇文章是我閱讀了A+ Promise implementing的筆記與心得。因為官方講解其實用語很精煉,所以我決定用我自己的話寫一篇容易看得懂的筆記。有任何錯誤歡迎留言指正。
PS: 我在本篇文章中交替使用resolve與議決這兩個詞彙。
建立物件內部變數
1 2 3 4 5 6 7 8 9 10 11 12 |
//Promise內部有三種狀態 var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise() { // 一開始的狀態是Pending var state = PENDING; // 一旦Promise被resolve,把成功的value或是失敗的error快取起來 var value = null; // handlers用於儲存 呼叫then或done的後success, failure的handler var handlers = []; } ``` |
建立轉換狀態的內部方法fullfill和reject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise() { var state = PENDING; var value = null; var handlers = []; function fulfill(result) { state = FULFILLED; //讓狀態改成成功 value = result; //快取結果 } function reject(error) { state = REJECTED; //讓狀態改成失敗 value = error; //快取錯誤 } } |
建立更高階轉換狀態的方法resolve
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise() { var state = PENDING; var value = null; var handlers = []; function fulfill(result) { state = FULFILLED; value = result; } function reject(error) { state = REJECTED; value = error; } // resolve接受一個單純的值或是一個Promise當作參數。 // 如果是個一般值,那就轉換狀態 // 如果是個promise,那就對該promise進行議決,等到議決結果出爐再透過callback轉換狀態。 function resolve(result) { try { //試圖取得該物件是否包含then方法,若有,則代表這是一個promise var then = getThen(result); if (then) { //如果傳入的值是個promise,那麼就透過doResolve先去議決該promise, //再根據議決的成功與否callback resolve or reject doResolve(then.bind(result), resolve, reject) return } fulfill(result); } catch (e) { //在resolve的過程中,如果catch到錯誤會讓該promise reject reject(e); } } } |
getThen
判斷傳入的值是不是promise,如果是的話回傳該promise的then方法,可以注意到檢查方式很鬆散,只是檢查有沒有then方法而已,這種方式可以讓多個不同的promise library彼此相容。
1 2 3 4 5 6 7 8 9 10 |
function getThen(value) { var t = typeof value; if (value && (t === 'object' || t === 'function')) { var then = value.then; if (typeof then === 'function') { return then; } } return null; } |
doResolve
實際進行議決,doResolve 有責任確保傳入的resolve和reject這兩個參數只有其中一個會被呼叫一次。
注意doResolve的工作,他會以傳入的fn進行議決,然後再根據議決結果呼叫傳入的onFulfilled或onRejected。並且使用了一個內部變數done來確保onFulfilled或onRejected只會被呼叫一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function doResolve(fn, onFulfilled, onRejected) { var done = false; try { fn(function(value) { if (done) return done = true onFulfilled(value) }, function(reason) { if (done) return done = true onRejected(reason) }) } catch (ex) { if (done) return done = true onRejected(ex) } } |
仔細觀察可以注意到,fn吃兩個參數,一個是成功時的callback,另一個是失敗時的callback,正好對應到Promise建立時的syntax
new Promise(function(resolve, reject) { ... });
建立Promise建構式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise(fn) { //傳入fn作為參數 var state = PENDING; var value = null; var handlers = []; function fulfill(result) { state = FULFILLED; value = result; } function reject(error) { state = REJECTED; value = error; } function resolve(result) { try { var then = getThen(result); if (then) { doResolve(then.bind(result), resolve, reject) return } fulfill(result); } catch (e) { reject(e); } } doResolve(fn, resolve, reject); //對fn進行議決 } |
整個promise基本設定完成後,執行最後一行doResolve,直接對new Promise(xxx)的xxx進行議決,要是議決成功就執行resolve,議決失敗就執行reject。
解釋
為什麼resolve要搞這麼複雜,還要透過doResolve來解決?因為一個promise被議決時有下列兩種情況:
1 2 3 4 5 6 7 8 9 10 11 12 |
//有一個非同步的promise, 會在一秒鐘之後議決成yeeeee var yeePromise = new Promise(function(resolve, reject) { setTimeout(function() { resolve("yeeeee"); //resolve一個value }, 1000) }) //在三秒鐘之後議決yeePromise var p1 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve(yeePromise); //resolve一個promise }, 3000) }); |
如果該promise發現他必須再resolve另一個promise(他具有then方法),那麼就必須繼續取得該promise議決的結果。取得結果的方法是呼叫該promise的then,一旦呼叫then後會有三種情況,沒事、呼叫onFulfilled callback,或是呼叫onRejected callback。
doResolve吃三個參數,fn(要議決的內容), onFulfilled(成功時的callback), onRejected(失敗時的callback),因此我們可以把要fn訂成該promise的then,也就是doResolve(then.bind(result), resolve, reject)。
bind會把執行then時的this綁定到該promise上,因此看起來就像呼叫了該promise的then,如果成功的話就繼續議決(resolve),如果失敗的話就否決(reject)
觀察Promise狀態
我們已經完成所有基本的工作了,現在唯一的問題是,我們沒辦法知道該promise到底有沒有乖乖把任務完成,因此我們需要.then來回報狀態。
但我們先來實作.done吧,因為.done比.then簡單一點
promise.done(onFulfilled, onRejected)
首先我們有幾個需求
- 只有onFulfilled或onRejected其中之一會被呼叫
- 只會被呼叫一次
- 他不會立刻被呼叫,而是會在done return之後之後才會被呼叫(非同步)。
- 不管我們的promise在call .done之前被議決或是.done之後,他就是會被呼叫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise(fn) { var state = PENDING; var value = null; var handlers = []; function fulfill(result) { state = FULFILLED; value = result; handlers.forEach(handle); //fulfill時,要執行每一個待執行的handler handlers = null; } function reject(error) { state = REJECTED; value = error; handlers.forEach(handle); //reject時,要執行每一個待執行的handler handlers = null; } function resolve(result) { try { var then = getThen(result); if (then) { doResolve(then.bind(result), resolve, reject) return } fulfill(result); } catch (e) { reject(e); } } function handle(handler) { if (state === PENDING) { handlers.push(handler); } else { if (state === FULFILLED && typeof handler.onFulfilled === 'function') { handler.onFulfilled(value); } if (state === REJECTED && typeof handler.onRejected === 'function') { handler.onRejected(value); } } } this.done = function (onFulfilled, onRejected) { // ensure we are always asynchronous setTimeout(function () { handle({ onFulfilled: onFulfilled, onRejected: onRejected }); }, 0); } doResolve(fn, resolve, reject); } |
.done透過setTimeout來達成非同步的效果,在next Tick之後才根據狀態執行handle看看(晚點再講為什麼要這麼做),然後根據狀態決定要先等待還是進行處理。如此一來,就可以透過傳入.done的callback來讓promise根據狀態決定是否執行任務了。
搞懂.done後再來就是大魔王.then了
Promise.then(onFulfilled, onRejected)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
this.then = function (onFulfilled, onRejected) { var self = this; return new Promise(function (resolve, reject) { return self.done(function (result) { if (typeof onFulfilled === 'function') { try { return resolve(onFulfilled(result)); } catch (ex) { return reject(ex); } } else { return resolve(result); } }, function (error) { if (typeof onRejected === 'function') { try { return resolve(onRejected(error)); } catch (ex) { return reject(ex); } } else { return reject(error); } }); }); } |
可以看到我們用了一個很漂亮的作法來實作.then,那就.then會回傳一個新的Promise。如此一來你就可以使用Promise Chain來串接).then(cb).then(cb).then(cb)
這裡超級精彩的:.then回傳了一個新的Promise,而這個Promise所答應的事情是「原本Promise的完成(done)」
光講不清楚,我們寫一段簡單的code就知道什麼意思了
1 2 3 4 5 6 7 8 9 10 |
//這是一個會在10秒後議決成'yeeeee'的Promise var yeePromise = new Promise(function(resolve, reject) { setTimeout(function() { resolve('yeeeee'); }, 10000) }) //我們呼叫了.then() yeePromise.then(function(result) { console.log(result); }) |
呼叫.then()後, .then()會回傳一個新的Promise,這個Promise會去呼叫yeePromise內部的.done(),而.done會先檢查yeePromise的狀態,發現是Pending,就先把handler放在handlers裡頭,直到5秒後yeePromise被resolve了,他才會執行剛剛保存的handler,最後印出'yeeeee'
為什麼.done內要setTimeout(fn, 0)
這是很重要的問題。請看下列程式碼
1 2 3 4 |
var promise = query(); A(); promise.then(query); B(); |
你預期會發生什麼事?
如果Promise是非同步的話,答案會是A() -> B() -> query()
如果Promise是同步的話,答案會是A()->query()->B()
為了避免讓程式設計師混淆,因此Promise的實作規格規定一定要是非同步的。
注意
另外請注意,.done並不是Promise/A+的實作標準規格,但大多數的標準Library會實作他。
以上就是我的筆記,希望這份筆記能夠讓你簡單的理解Promise是如何實作的。