0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Grimoire.jsAdvent Calendar 2016

Day 18

Effective Grimoire.js 2016

Last updated at Posted at 2016-12-24

Grimoire.js をより便利に、効果的に扱うため、あるいはバグの対処のために覚えておくといいことを7個紹介する。

少し踏み入った内容になるので、是非Grimoire.jsって何?って方は、とりあえず**こちら**を参照していただきたい。

フロントインターフェース編

ノード・コンポーネントの登録のタイミングに気をつけよ

Grimoire.jsのタグツリーが構築されると、gr(function(){})によって登録したイベントハンドラが呼び出される。
Grimoire.jsを扱ったことがある方なら、以下で表示されたタイミングから1秒後にメッシュの色が赤くなることがわかるだろう。
(以下、gomlのscriptタグにid="main"が付いていると仮定する)

gr(function(){
   gr("#main")("mesh").setAttribute("color","red");
});

この、grが呼び出されるのは、必ずGrimoire.jsのロード時に存在したHTML上のtype="text/goml"が指定されたscriptタグにより指定されたGOMLのツリーが構築された直後である。
したがって、コンポーネントやノード登録の処理を以下のように書くのは無効である。

gr(function(){
   gr.registerComponent("SomeComponent",ComponentConstructor);
   gr.registerNode("some-node",[...],{...});
   gr("#main")("mesh").setAttribute("color","red");
});

なぜなら、この関数が実行されるのはツリー構築後なのだ。もしも、GOML内にこれらを使うコードがあるなら例外を出すことになってしまうだろう。したがって、これは以下のようにgrの外に書くべきだ。

gr.registerComponent("SomeComponent",ComponentConstructor);
gr.registerNode("some-node",[...],{...});
gr(function(){
   gr("#main")("mesh").setAttribute("color","red");
});

first()よりもsingle()を使え

特定のノードの特定のコンポーネントをフロントインターフェース側で取得したいケースが稀に存在する。
このような場合、以下のように書けば良い。

   const c = gr("#main")("mesh").first().getComponent("コンポーネント名");

gr("#main")("mesh").first()によって、見つかった中で一番最初のノードを返してくれる。このノードに対してgetComponentをするわけだ。

しかし、同じノード名はどうしたって存在するし、思ったようにクエリは動いていないかもしれない。
本当は一つだけを絞っているつもりが複数個取れてしまってもバグになる。

もし、これが0個しかとれないならば、nullになるので例外ですぐにわかることであろう。しかし、first()を使ってしまうと、思ったものが2つ存在して、片方だけ実行しても問題は全くない。

しかし、もし取れるものが必ず一つだと思う場合はsingle()を代わりに使うといいだろう。もしもこの時2つ以上存在すると例外をだす。

   const c = gr("#main")("mesh").single().getComponent("コンポーネント名");

クラス参照の取得を活用せよ

Grimoire.jsの開発中では、各tsファイルは別々に記述されており、内部的にはimportrequireでそれぞれの参照を取得している。
Grimoire.jsを利用する場合はスクリプトタグあるいはnpmを用いて利用することができるが、そのどちらをとっても、ほぼ全てのGrimoire.jsでの内部のファイルのデフォルトのエクスポートへアクセスすることができる。

取得の対象は、取得する対象がコアの中に定義されているものか、grimoireのプラグインの中に定義されているものかによって少し取得方法が異なる。

grimoirejsのコアのそれぞれのファイルのデフォルトエクスポートへのアクセス

例えば、Grimoire.js内でsrc/Node/Componentに定義されている、Componentクラスへは以下のような形式でアクセスできる。

ES6での取得例

index.ts
import Component from "grimoirejs/ref/Node/Component";

scriptタグからの取得例

index.js
const Component = gr.Node.Component;

grimoirejsのプラグインのそれぞれのファイルのデフォルトエクスポートへのアクセス

Grimoire.jsではベクトル型でさえもプラグインとして登録されている。普段、grimoirejs-preset-basicという、最低限のプラグインなどが含まれているパッケージを利用する方が多いと思うが、この中に含まれているgrimoirejs-mathこそがベクトルなどのクラスを管理しているパッケージである。

例えば、3次元ベクトルを管理するクラスであるsrc/Vector3に定義されているVector3は以下のように取得することができる。

ES6での取得例

index.ts
import Vector3 from "grimoirejs-math/ref/Vector3";

scriptタグからの取得例

index.js
const Vector3 = gr.lib.math.Vector3;

コンポーネント系

attributeの変数とのバインドは$awakeで行え

もし、attributeの中の変数を、コンポーネント内にバインドするなら$mountで行うか、$awakeで行うか悩む場合がある。
しかし、もし、コンポーネント自身にバインドするなら、それがどのノードに属しているかに寄らず、コンポーネントの変数に代入してほしいはずである。

あるコンポーネントに最初に呼ばれるメッセージ$awakeは、再度マウントされたりしても2度と呼ばれることはない。最初の一度だけ呼ばれることが保証されるメッセージである。

したがって、ある変数をバインドするなら、以下のようになるだろう。

gr.registerComponent("ABC",
   attributes:{
      someAttribute:{
        converter:"String",
        default:""
     }
   },
   $awake:function(){
      this.getAttributeRaw("someAttribute").boundTo("_someAttribute");
   }
});

これ以降は、このコンポーネントの中ではthis._someAttributeによりアクセスできるので、いちいちthis.getAttributeを呼ぶ必要はない。
これは、属性の検索や、コンバーターの呼び出し回数の削減につながるので多くの場合、コンポーネントの最適化にもつながる。

DOMのイベントハンドラのadd,deleteには気をつけよ

もし、HTMLのDOM要素などに対して、addEventListenerなどをする場合、参照カウント式GCをもつjavascriptの特性から、破棄したはずのコンポーネントが生き残ってしまう可能性がある。

例えば、以下のようなコンポーネントを作成したとしよう。

gr.registerComponent("ElementClickObserver",
   attributes:{
      elemQuery:{
        converter:"String",
        default:"div"
     }
   },
   $mount:function(){
      const elemQuery = this.getAttribute("elemQuery");
      document.getElementsByQuery(elemQuery).item(0).addEventListener("mouseclick",()=>{
         // elemQueryを含んだ処理など
      });
   }
});

このmountではイベントを登録しているが、仮にコンポーネントが消されたとしても、このunmountには何の処理も書いておらず、エレメントから、このコンポーネント内のハンドラまでの参照が残り続ける。また、このハンドラは何らかのコンポーネントの変数に参照を残しているとすれば、このハンドラがGCによって回収されない影響でコンポーネントも回収されない。

もし、頻繁にコンポーネントが追加されたり、削除されたりするなら、このようなGC上のメモリリークに気をつける必要がある。
イベントは、いらなくなったら消そう。

是非、$unmount$disposeメッセージを活用してほしい。

コンストラクタ形式でのgetComponentを活用せよ

Unityを利用している方にとっては直感的だと思うが、コンポーネントはポリモーフィズムを持った活用方法をとることによってより一層の価値を発揮するようになる。

例えば、TransformComponentを継承したコンポーネントTransformExtendedを作成したとしよう。

index.js
gr.registerComponent("TransformExtended",
{
  $update:function(){

  }
},"Transform");

この場合、Transformで定義された$updateを書き換えることができる。コンポーネントとして保つべきインターフェースを保てば、コンポーネントを参照する側は必要以上の実装を気にかけずに、中身の実装が異なるそれぞれのコンポーネントを取得できる。

js.index.js
const Transform = gr.lib.fundamental.Components.TransformComponent;
const transforms = this.node.getComponentsInChildren(Transform);

この、以下のtransformsの中にはTransformを継承するコンポーネントならば全て入りうる。これにより、ポリモーフィズムを維持したコンポーネントの活用が可能だ。
もし、これがTypescriptならば、さらにジェネリクスによって、transformsTransform[]であることも認識されてさらに便利である。

イベント目的で自分のノードにsendMessageは代わりにeventEmitterを用いよ

もし、自分のノードに対してイベント目的でsendMessageを呼び出す、つまり何らかのタイミングで同じノードについている他のコンポーネントに対してsendMessageを用いて通知したい場合、eventEmitterを用いるべきだ。

sendMessageはフロントインターフェース側からはハンドリングすることはできない。ノードのライフサイクル上でノード間の連携のために存在する機能である。もし、単にイベントを通知する目的でsendMessageを用いるなら、eventEmitterを用いることによって、以下の利点が得られる。

  • 若干のパフォーマンスの向上
  • フロントインターフェース側からのハンドリング
index.js
  this.node.sendMessage("some-event",args);

これは、以下のように描き直すべきだ。

index.js
   this.node.emit("some-event".args);
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?