taediumの日記

2011-10-02

[] ViewModelの定義パターン

まず、コンストラクタを使うかどうかで迷って、使わない場合にはdependentObservableからViewModelをどう参照するかに迷います。

どれがいいんでしょうねー?


パターン1: コンストラクタを使う

まぁ、ふつうです。

ただ、コンストラクタ呼び出しはnewを忘れると気づきにくいから避けるべきみたいな言説もあり、その点でどうしよう?と思います。

var ViewModel = function() {
    this.arg1 = ko.observable(0);
    this.arg2 = ko.observable(0);
    this.result = ko.dependentObservable(function() {
        var arg1 = parseInt(this.arg1(), 10);
        var arg2 = parseInt(this.arg2(), 10);
        return arg1 + arg2;
    }, this);
};
ko.applyBindings(new ViewModel());

パターン2: コンストラクタを使わない & dependentObservableからはthisでViewModelを参照

コンストラクタを使わない場合はこんな感じ。dependentObservableの第2引数でViewModelを渡して、第1引数の関数からthisでアクセスできるようにしています。

var viewModel = {
    arg1: ko.observable(0),
    arg2: ko.observable(0)
};
viewModel.result = ko.dependentObservable(function() {
    var arg1 = parseInt(this.arg1(), 10);
    var arg2 = parseInt(this.arg2(), 10);
    return arg1 + arg2;
}, viewModel);
ko.applyBindings(viewModel);

パターン3: コンストラクタを使わない & dependentObservableからはクロージャでViewModelを参照

thisが何を指しているかわかりにくいとか、dependentObservableの第2引数でviewModelを渡すのを忘れがちだとかを問題にするなら、こう。

var viewModel = {
    arg1: ko.observable(0),
    arg2: ko.observable(0)
};
viewModel.result = ko.dependentObservable(function() {
    var arg1 = parseInt(viewModel.arg1(), 10);
    var arg2 = parseInt(viewModel.arg2(), 10);
    return arg1 + arg2;
});
ko.applyBindings(viewModel);

パターン4: コンストラクタを使わない & dependentObservableからはクロージャでViewModelを参照 & dependentObservableを{}中で定義

パターン2とパターン3はdependentObservableをobservableと一緒に定義できないのがいやなんですよねー。一緒に定義するには、評価を遅延する必要があります。

(デフォルトのままだと、dependentObservableに渡す関数が実行されるタイミングでまだViewModelがundefinedなので)。

var viewModel = {
    arg1: ko.observable(0),
    arg2: ko.observable(0),
    result: ko.dependentObservable(function() {
        var arg1 = parseInt(viewModel.arg1(), 10);
        var arg2 = parseInt(viewModel.arg2(), 10);
        return arg1 + arg2;
    }, null, {deferEvaluation: true})
};
ko.applyBindings(viewModel);

結論

コンストラクタを使うのが一番シンプルな気がします。


[] dependentObservableの仕組み

Knockout.jsを触り始めて最初に感じた疑問は、dependentObservableって何で依存先の変更を自動で感知できるのか?でした。

コードを読んでわかってきた自分なりの理解をまとめます。

例として、簡単な足し算アプリを使います。

コードはこちらにも貼り付けておきます。

HTML
<input data-bind="value: arg1" /> +
<input data-bind="value: arg2" /> =
<span data-bind="text: result">
JavaScript
var ViewModel = function() {
    this.arg1 = ko.observable(0);
    this.arg2 = ko.observable(0);
    this.result = ko.dependentObservable(function() {
        var arg1 = parseInt(this.arg1(), 10);
        var arg2 = parseInt(this.arg2(), 10);
        return arg1 + arg2;
    }, this);
};

ko.applyBindings(new ViewModel());

ViewModelを見てもらうとわかりますが、resultプロパティがdependentObservableです。dependentというからには何かに依存しているのですが、何に依存しているかというとarg1とarg2のプロパティです。

でも依存元のresultと依存先のarg1やarg2を紐つける明示的なステートメント(arg1.subscribe(result);みたいな)はどこにもありません。でも、arg1やarg2に対応するTextBoxの値が変更されると、それに連動してresultに対応するspanタグの値も変わります。これはいったいどういう仕組みだろう?と思ったわけです。

結論から言えば、dependentObservableの第一引数に渡した関数(以降Xと呼びます)が最初に実行するタイミングで紐つけ処理が行われています。どういうことかというと、Xが実行中の間にobservableからデータを読み取る処理(this.arg1()など)が行われたら、Xをobservableにsubscribeします。で、一度subscribeした後は、observableにデータを書き込む処理(this.arg1(100)など)が行われたら変更をXにnotifyするようになります。Xそのものがsubscribeされるわけではないのですが、概略としてはそんな感じです。

ちょっと乱暴に言ってしまうと、ko.observableやko.dependentObservableは、値のRead/Writer処理をフックしていろいろやってくれるということです。その結果、明示的に書くと繰り返しが多くなってしまいがちな処理が不要になるんですね。

ところで、Xの中で依存先のobservableな値を書き換えたら、呼び出しの循環が起きそうですが、その辺はちゃんとチェックしていて、そのようなことをしてもXは一度しか呼び出されないようになっていました。

ちなみにですが、今回は、observableとdependentObservableにおける値の変更の通知に注目しました。observableやdependentObservableの値とDOMの要素における値の変更通知はまた別の仕組みで行われています(バインディングと呼ばれている)。