昨天在某固定會逛的 Facebook 社群看到有人問了 JavaScript 的問題,需求大致上是:
想要建立一個 NPC 的物件,對外提供幾個 API 來使用
- OnTrigger() - 讓 NPC 出現在畫面上
- OnDialog() - NPC 秀出對話框,對話框內有一個確認鈕
- OnWalk() - 若對話框曾經開啟並關閉之後,將 NPC 移動至某特定位置
希望的執行方法是(連續動作):
- OnTrigger();
- OnDialog();
- OnWalk();
很明顯的主要的困難處在於
OnDialog()
及OnWalk()
是照順序執行,要如何能夠讓實際的OnWalk()
內容在OnDialog()
有了某種結果之後才執行?
建立 Npc
首先先把比較沒有問題的部分做出來,建立一個可以new
的Npc
function,並加上面所列的 API 們。
var Npc = function () {
this.init();
};
Npc.prototype.init = function (npcElm, dialogElm) {
// init things
};
Npc.prototype.OnTrigger = function () {
show();
};
Npc.prototype.OnDialog = function () {
createDialog();
showDialog();
};
Npc.prototype.OnWalk = function () {
// what now?
};
運用 jQuery 提供的 promise 功能 - $.Deferred
首先需要讓OnDialog
這回事兒在Npc
內是一個「未完待續」的狀態。
Npc.prototype._dialogPromise = null;
Npc.prototype.OnDialog = function () {
var dfd = $.Deferred();
this._dialogPromise = dfd.promise(); // leave a promise in Npc instance
createDialog();
showDialog();
};
留下「未完待續」的狀態之後,當然也必須要讓該狀態可以「完結」;在對話框內的按鈕被點擊之後,主動將狀態改成「完結」。
Npc.prototype.OnDialog = function () {
var dfd = $.Deferred();
this._dialogPromise = dfd.promise(); // leave a promise in Npc instance
createDialog();
showDialog();
onDialogButtonClick(function () {
dfd.resolve();
});
};
最後,將OnWalk()
的實際執行擺在對話框狀態「完結」之後。
Npc.prototype.OnWalk = function () {
// Do not walk if dialog hasn't been triggered
if (!this._dialogPromise) {
return;
}
this._dialogPromise
.then(function () {
walkToDestination();
});
};
在OnDialog()
之後,無論是 先關掉對話框再OnWalk()
或 先OnWalk()
才關掉對話框 ,效果都是一樣的。
更進一步
如果希望呼叫幾次OnWalk()
就會移動幾次,透過 promise chaining 的功能來形成 queue 就可以做到了。
Promise chaining queue 的基本 pattern 如下:
promise = promise.then(function () { /* something async */ });
於是修改Npc.prototype.OnWalk
。
Npc.prototype.OnWalk = function () {
// Do not walk if dialog hasn't been triggered
if (!this._dialogPromise) {
return;
}
this._dialogPromise = this._dialogPromise
.then(function () {
walkToDestination();
});
};
連續呼叫OnWalk()
後會發現walkToDestination()
跑了相同的次數,而畫面上的移動卻只發生一次。
關鍵在於必須要讓「走到特定位置」這回事兒變成同樣可以追蹤始末的 promise,修改Npc.prototype.OnWalk
的核心內容如下:
this._dialogPromise = this._dialogPromise
.then(function () {
// start walking
walkToDestination();
var dfd = $.Deferred();
// at destination
onDestination(function () {
dfd.resolve();
});
return dfd.promise();
});
在下面,用OnDialog()
打開對話框之後,多按幾次OnWalk()
再關閉對話框,就會出現連續動畫的效果。
後記
如果本次分享的 case study 有略為引起你對 promise 的興趣,而想了解更多的話,這邊有一些不錯的推薦連結: