Skip to main content

JavaScript Promise 實作理解筆記

// 本文撰寫於 2015 年

如何實作JavaScript Promise?

本篇文章是我閱讀了A+ Promise implementing的筆記與心得。因為官方講解其實用語很精煉,所以我決定用我自己的話寫一篇容易看得懂的筆記。有任何錯誤歡迎留言指正。 PS: 我在本篇文章中交替使用resolve與議決這兩個詞彙。

建立物件內部變數

//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

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

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彼此相容。

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只會被呼叫一次。

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建構式

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被議決時有下列兩種情況:

//有一個非同步的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)

首先我們有幾個需求

  1. 只有onFulfilled或onRejected其中之一會被呼叫
  2. 只會被呼叫一次
  3. 他不會立刻被呼叫,而是會在done return之後之後才會被呼叫(非同步)。
  4. 不管我們的promise在call .done之前被議決或是.done之後,他就是會被呼叫
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)

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就知道什麼意思了

//這是一個會在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)

這是很重要的問題。請看下列程式碼

var promise = query();
A();
promise.then(query);
B();

你預期會發生什麼事? 如果Promise是非同步的話,答案會是A() -> B() -> query() 如果Promise是同步的話,答案會是A()->query()->B() 為了避免讓程式設計師混淆,因此Promise的實作規格規定一定要是非同步的。

注意

另外請注意,.done並不是Promise/A+的實作標準規格,但大多數的標準Library會實作他。 以上就是我的筆記,希望這份筆記能夠讓你簡單的理解Promise是如何實作的。

reference