なぜBackbone.Modelのchangeイベントが発火しないかについての2つの陥っていがちなパターン

  • 10
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

おきまりのやつ

どうも、Backbone.js Advent Calendar 2014及びCYBIRDエンジニア Advent Calendar 2014 3日目の@dekokunです。

昨日は、Backbone.jsのほうが@okameさんのBackbone.Routerに有限状態遷移機械を簡単に実装できるライブラリを作ったでした。ちょうど、画面遷移の際のアニメーション設計をしていたところなので、実装の参考にさせていただきます!!!
CYBIRDのほうは@t-kashimaくんの平成生まれとインターネットでしたね。平成生まれの後輩がいるっていうのがまず衝撃ですよね。処理部、いいですね。私も処理部入りたかった。

本日の内容を1行で

Backbone.Modelのsetメソッドは現在のattributeとsetするattributeを_.isEqualメソッドで比較して値が変化したかどうか(=changeイベントを発火するかどうか)を決めるよ!あと、最後にポエムもあるよ!

2行になってしまった。

以下のブログは、上記最初の1行を例を出しながら説明しているだけですので、上記最初の1行の意味が分かる人は以下を読んでも何も得られないでしょう。
そういう人は最後のポエム「私とBackbone.Marionette」だけ読むといいと思います。

「changeイベントが発火しない」という場合に陥っていがちな2つのパターン

attributeから取り出したものと同じオブジェクトをsetしている

文字通りです。これは、JSに慣れている人なら特に引っかからないとは思いますが、PHPみたいに、辞書形式のデータ構造は基本的にクローン(一応PHPerとしてPHPを擁護しておくと、クローンと言っても、COWだよ!)されていくような感じの言語に慣れてる人はやりがちかも。

var hoge = model.get('hoge');
hoge.fuga = 1;
model.set('hoge', hoge); // change:hoge イベントが発火しないよ!!!

対処策

cloneしましょう

var hoge = model.get('hoge');
var hogeClone = _.clone(hoge); // ここで別のオブジェクトにしてあげるのが肝要
hogeClone = 1;
model.set('hoge', hogeClone); // change:hoge イベントが発火した!やったー!!!

attributeから取り出した配列を_.mapなどで変換してからsetする際に、_.mapが一つ一つの要素と同じオブジェクトを返している

これが、比較的ハマりました。以下の様な感じです。

var hoge = model.get('hoge'); // hogeは配列
var hogeChanged = _.map(hoge, function(value) {
  value.fuga = 1;
  return value;
});
model.set('hoge', hogeChanged); // change:hoge イベントが発火しないよ!!!

チームのメンバーから「なんかイベント発火しないんすけど〜」と連絡があり、「あれ、_.mapで変換しているからsetしているのは元のとは別のオブジェクトだよな〜なんでだなんでだ(同じオブジェクトかどうかの比較をしているだけだと思っていた)」ってなっていました。

対処策

cloneしましょう

var hoge = model.get('hoge'); // hogeは配列
var hogeChanged = _.map(hoge, function(value) {
  var valueClone = _.clone(value); // ここで別のオブジェクトにしてあげるのが肝要
  valueClone.fuga = 1;
  return valueClone;
});
model.set('hoge', hogeChanged); // change:hoge イベントが発火した!!!

なぜなのか

上記ありますように、Backbone.Model#setは、単純にsetされたらイベントを発火するわけではなく、_.isEqualで過去の値と比較して変化があったものだけイベントを発火しているからなのです。

参考:https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L344

_.isEqualの挙動

_.isEqualは内部的にはeqという関数を呼び出していますので、eqの動きの説明です。

1 . まず、比較する2つのものが===で比較してtrueであればreturnします(正確には、0と+0の時だけ違うんだけど、まぁ今回の話には関係ない)

参考:https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L906

2 . その後いろいろ比較して(あまりちゃんと調べてないです。。)、両者が配列であれば、再帰的にeqを呼び出します。

参考:https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L967

というわけで、陥りがちな2つのパターンにおいて、「attributeから取り出したものと同じオブジェクトをsetしている」は、上記1. の比較によって値が変化していないとなっておりchangeイベントが発火しない、「attributeから取り出した配列を_.mapなどで変換してからsetする際に、_.mapが一つ一つの要素と同じオブジェクトを返している」は、上記2.で再帰的な比較に入って上記1.の比較で値が変化していないことになってchangeイベントが発火しない、ということなのでした〜。

なお、上記はBackbone.js 1.1.2及びUnderscore.js 1.6.0で調査しています。

あとがき

最初は、「Backbone.Marionetteのイベント一覧を書く」と言っていたのですが、既に公式のドキュメントに、クラス毎ではあるもののわかりやすいイベント一覧もありましたので、今回は、結構ハマりそうな割に日本語文献はあまりないBackbone.Modelのイベントが起きないパターンについて書いてみました。

正直、「Backbone書いてる人ならそんなんだれでも知ってるよ」的内容であるといえばあるのですが(Backbone.jsは非常にソースコードを追いかけやすいし)、まぁ、人生は有限、「Backboneの内部構造なんて全然知らないけどググりながら解決してちょっとしたアプリを作りたい」みたいな人の人生の時間を無駄にしないためにも、ネットの海にこのような情報を流しておくのは悪いことではありますまい。

ものたりない人のために、以下ポエムを載せておきます。

私とBackbone.Marionette(ポエム)

  • 本当にただの、私個人に関するポエムなので注意してください
  • ちゃんと読み返したら、Backbone.Marionette自体の話はほとんど書かれていなかったので注意してください

思い返せば6年前、@dekokunこと私は、新卒入社でこの会社に入り、メール配信システムのメンテナンスに携わる日々が始まったのであった…

いろいろあった…Perlで作られたメールのメールデータ作成システムと、PHPで作られたメール配信の管理画面と、なにで作られているかよくわからないけど何かで作られた携帯メール配信に特化したMTAのメンテナンスを行う日々であった。

当時は「自分は全く仕事ができない」という思いをものすごく強く受けつつ、夜も、翌日仕事に行くことを考えるとベッドに入って寝るのが恐ろしく(寝たら朝になってしまう!そしたら仕事に行かなくてはいけない!!!)(余談であるが、私はベッド派である。実家では布団だったが、まぁ、ベッド派である。まぁ、布団でも問題なく寝られるけど)、激しい寝不足の日々を過ごしていた…

そんなこんなで6年、いろいろなことがあって、ISUCONの予選を通過できる程度には立派なサーバサイドエンジニアになったのであった。

1年前も同じ事言ってる

めでたしめでたし。

しかし私には悩みがあった…
「私はGUIプログラミングを全然したことがない!!!!!!!!」

GoFのデザインパターンに代表される、複雑な状態を扱うための、というか複雑な状態を明示的に扱わなくても済むための数々のプログラミングテクニックがこの世にはたくさん存在する。
しかし、基本的に「DB以外はあまり状態を持たず、1リクエスト毎にプログラムのインスタンスが破棄される」サーバサイドのプログラミングを行う限り、そのあたりの設計にどれだけ失敗しても、GUIプログラミングほど致命的な問題にならないと私は感じている。もちろん、サーバサイドはサーバサイドで、並列に大量のリクエストをさばく必要があるという性質上、設計に失敗すると性能劣化という意味での致命的問題が発生する場合が多々あるわけだが、まぁ、つまり何が言いたいかというと、自分でもよくわからなかったけど、ま、とにかくGUIプログラミングがしたかったんです!

GUIプログラミングの本質的な状態の多さ、それによる難しさ、それに私は魅せられていたのである(こんなことを言っているが、上記にも書いたようによくわからないし、実際のところはただたんにサーバサイドプログラミングの、性能問題に対する複雑さについての理解が深まりほんの少し乗りこなせるようになった感じがしたから、GUIプログラミングの方に目移りしたというくらいの話なのではないかと私は自分を分析している)

(なお、実際にJSのプロジェクトに入って数ヶ月経ち、複雑さ、状態の多さに辟易しているので、DOM操作が死ぬほど早くなるかVirtualDOMが死ぬほど発達するかして、「現在何が描画されているべきか」だけを管理していく日々が早くこないかなって思ってます)

そんな中、会社で「ブラウザ上で動く方のJSでゲームをつくろう(回りくどくて意味不明な言葉ですが、つまり、サーバサイドJSじゃないほうのJSっていうことです)」というプロジェクトが立ち上がり、私にも参加しないかと声がかかった!!!!

というわけで、私はめでたくJSでゲーム開発という、GUIプログラミングバリバリの環境に身を置かれることになったのであった。

めでたしめでたし。

そして、JSのフレームワーク選択…

いろいろあって、Backbone.Marionetteになりました。私が強く推したフレームワークだったので、これは頑張ろうとおもったのでした。

今ではBackbone.Marionetteと仲良く暮らしています。

めでたしめでたし。

Backbone.Marionette、Backbone初心者ならView周りのベストプラクティスを知るためにも是非導入するといいと思います!CompositeViewとか、たまげました。

ただ、「現状からの差分を記述するのではなく、Modelの状態が変化したタイミングでそのモデルに関連するViewを描画する」というのを性能を担保しながらちゃんとやろうとすると、Viewはどんどん切り刻まれていき、なんか違うなって感じになるので(まぁ、これは別にBackboneだからという話でもないですが)、まぁ、これからのブラウザのDOM描画速度周りもしくはVirautlDOM周りの技術革新に私は期待しております。

なお、余談ですが(このポエム自体全てが余談といえばそのとおりですが)、最近数年は、仕事が楽しいので、夜も早く寝られるようになりました。

全てがめでたしめでたし。予定調和ですね。これぞ人生。

あとがきその2

Backbone.js Advent Calendar 2014の次回の人がいない!!やばい!!!!
CYBIRDエンジニア Advent Calendar 2014の明日の人は、アプリエンジニアの高◯さんです。弊社内のLTイベントなどでも最近発表しまくっている高◯さんに乞うご期待!!

初めて、Qiitaで書いてみた。書きやすい。私のブログとのかき分け、どうするかなぁ。

この投稿は CYBIRDエンジニア Advent Calendar 20143日目の記事です。