$A.runの廃止と$A.getCallbackの注意点、およびちょっとした改善

  • 6
    Like
  • 0
    Comment
More than 1 year has passed since last update.

経験豊かなSalesforceの開発者のみなさんは、止むに止まれぬ理由によりLightningコンポーネントの開発をする羽目になったとしても、不自由フレームワークから巧みに逃れ、巷で定評のあるオープンなフレームワーク/ライブラリを使用して生産性をガンガンに上げていらっしゃることでしょう。

さて、なぜそのような自由が謳歌できていたのかというと、Lightningフレームワークにはフレームワークの管理コンテキストの外からでもLightningの機能(サーバ呼び出しやイベントなど)を呼び出すことができる機能があったからです。これは$A.runという関数なのですが、Winter'16 から$A.runが廃止され代わりに$A.getCallbackを使うようになりました。

しかしながら、まだ$A.getCallbackの使い方はマニュアルにちょっとあるだけなので、どのようにしたらよいかわからない方も多いでしょう。

$A.getCallbackは、単に$A.runの置き換えというわけではありません。ちょっと奇妙な制約が設けられています。これはLightningフレームワークのAura Loopというひどい独特な設計が所以なのですが、そのことが分かるまで開発者はかなり苦労することになるでしょう。

例えば、5秒タイマーの後にLightningコンポーネントの属性をセットしたいとき、$A.runを利用した場合はこんな感じにしていました。

window.setTimeout(function() {
    $A.run(function() {
       cmp.set("v.visible", true);
    });
}, 5000);

Lightningフレームワークでは、すべてのコンポーネント操作はAura Loopと呼ばれるコンテキスト内で行われる必要があるという傲慢な斬新な制約があるので、このような事を行う必要があるのです。おそらく$A.runの動きとしては、引数で受け取った処理を実行キューに入れて、次のAura Loopが回ってきた時に実行するように指定する、といったイメージが近いかもしれません。

一方、$A.getCallbackは、関数を引数として受け取り、新たな関数を返す関数になっています。下はマニュアルにもあるコード例です。

window.setTimeout(
    $A.getCallback(function() {
        if (cmp.isValid()) {
            cmp.set("v.visible", true);
        }
    }),
    5000
);

コンポーネントへのアクセスを実行する前にcmp.isValid()を呼び出して状態チェックが追加されていますが、そこは本題ではないのでとりあえずおいておきます。

マニュアル等には明示的に書かれていないのですが、もっとも重要な点は、$A.getCallbackを呼び出すときは、必ずAura Loopにはいっている必要があるということです。$A.runはどのコンテキストから呼んでも大丈夫でしたが、$A.getCallbackはそうはいきません。

つまり、単に$A.runを置き換えるような以下のコードでは駄目ということです。実際実行するとエラーが発生します。

window.setTimeout(function() {
    // この処理が実行されている時点ですでにAura Loopの外側になっているため、$A.getCallbackは利用できない
    $A.getCallback(function() {
       if (cmp.isValid()) {
            cmp.set("v.visible", true);
       }
    })();
}, 5000);

もちろん、コンポーネントにアクセスするのですからコールバック関数を生成するときはすでにAuraのコンテキストに入っている場合がほとんどかと思いますが、多段の非同期呼び出しコールバックが介在しているときにはすべての段でコールバック関数をWrapしていかなければいけません。

getFooAsync($A.getCallback(function(foo) {
  getBarAsync(foo, $A.getCallback(function(bar) {
    getBazAsync(bar, $A.getCallback(function(baz) {
       if (cmp.isValid()) {
          cmp.set("v.foo", foo);
          cmp.set("v.bar", bar);
          cmp.set("v.baz", baz);
       }
    }));
  }));
}));

これはちょっと大変ですし、もう少し複雑になってくるとちゃんとすべての関数コールバックがトラックできているのかどうか不安になってくるでしょう。

しかたがないので、自作で$A.run相当を作ってみます。以下のmyAuraRun関数は、$A.runと同様に、関数を引数として受け取り、処理をAura Loop内で実行します。

  // 最初の定義はAura Loop内であることが前提
  window.myAuraRun = $A.getCallback(function(fn) {
    // getCallbackでwrapされた処理はAura Loop内で実行されるため、fnの実行もAura Loop内になる
    if (cmp.isValid()) { fn(); }
  });

  // 以下、Aura Loopの外から必要になった時の呼び出し方
  setTimeout(function() {
    window.myAuraRun(function() {
      cmp.set("v.visible", true);
    });
  }, 5000);

上記myAuraRunの定義自体は最初にAura Loop内でやっておく必要はありますが、定義されたmyAuraRunはAura Loop以外のどこから呼び出しても大丈夫になりました。めでたしめでたし?

というか、たとえ$A.getCallbackの仕組みが内部的に必要になったのだとしても、上記の方法でちゃんと代替できるのに、なんでわざわざ$A.runを廃止したんでしょうかねえ。