一個 NPC 的故事 - Case Study of Promise

昨天在某固定會逛的 Facebook 社群看到有人問了 JavaScript 的問題,需求大致上是:

想要建立一個 NPC 的物件,對外提供幾個 API 來使用

  1. OnTrigger() - 讓 NPC 出現在畫面上
  2. OnDialog() - NPC 秀出對話框,對話框內有一個確認鈕
  3. OnWalk() - 若對話框曾經開啟並關閉之後,將 NPC 移動至某特定位置

希望的執行方法是(連續動作):

  1. OnTrigger();
  2. OnDialog();
  3. OnWalk();

很明顯的主要的困難處在於

OnDialog()OnWalk()是照順序執行,要如何能夠讓實際的OnWalk()內容在OnDialog()有了某種結果之後才執行?

建立 Npc

首先先把比較沒有問題的部分做出來,建立一個可以newNpcfunction,並加上面所列的 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 的興趣,而想了解更多的話,這邊有一些不錯的推薦連結: