みなさんこんにちは。江口です。
今回は Knockout のバインド、自動 UI 更新機能を支える Observable について解説します。Knockout のバージョンは執筆時点で最新の安定版の 2.3.0 です。
Observable による状態の監視
これまでのエントリでも紹介したとおり ViewModel は View の状態に関する情報を持っているため、ViewModel は状態が変更された場合に View と同期する必要があります。その仕組みを実現するのが今回解説する Observable です。ViewModel のプロパティを Observable として定義することで、Knockout は ViewModel の状態が変更されたことを検知し UI を自動更新します。
Observable には用途に応じて使い分けられる次の三種類の Observable が用意されています。下の図のように、ObservableArray と Computed Observable(Dependent Observable) については Observable の機能を継承しているので、Observable のメソッドも呼び出すことができます。
- Observable
- ObservableArray
- Computed Observable(Dependent Observable)
Observable
observable を生成するには ko.observable メソッドを呼び出します。呼び出し時には引数を与えることで初期値を設定することができます。引数を指定しなかった場合の初期値は undefined になります。下の例では、初期値が「initial value」という文字列のプロパティ value を vm オブジェクトに設定しています。
var vm = { value: ko.observable('initial value') };
Observable を更新する
observable の状態を変更するには、新しい値を引数に指定して observable を関数として呼び出します。状態を変更すると、その observable を監視している UI 上の要素が自動更新されます。下の例では、value の値を「new value」に更新しています。
vm.value('new value');
なお、新しい値の型が undefined / boolean / number / string で同じ値の場合、observable は変更を通知しません。その他の型(object / function)の場合は、同じ値であっても常に変更として通知されます。このあたりの比較処理は observable の equalityComparer メソッドに定義されています。
vm.value('new value'); vm.value('new value'); // string 型かつ同じ値なので変更とみなされない var obj = {}; vm.value(obj); vm.value(obj); // object 型なので同じオブジェクトでも変更とみなされる
observable は関数として呼び出されることで自身の変更を検知しています。そのため、observable に格納したオブジェクトを直接操作すると、その変更を検知することができないため UI は更新されません。そのような操作をする必要がある場合は、そのオブジェクトのプロパティにも observable を使用します。
var vm = { value: ko.observable({ text1: 'hello', text2: ko.observable('hello') }) }; vm.value().text1 = 'good bye'; // UI は更新されない vm.value().text2('good bye') // UI は更新される
Observable から値を取得する
observable から値を取得するには引数を指定せずに observable を呼び出します。下の例では、vm.value の値を取り出しています。
var vm = { value: ko.observable('initial value') }; var test = vm.value();
サブスクリプション関数を追加する
observable には、状態の変更がされた時にその変更を受取るサブスクリプション(subscription)関数を追加することができます。サブスクリプション関数を追加するには、以下のように observable の subscribe メソッドを呼び出します。また、第二引数にはサブスクリプション関数内で使用する this に束縛する値、第三引数には検知するイベント名を指定することができます。下の例では、vm.value にサブスクリプション関数を追加しています。vm.value の値が変更されると、新しい値がコンソールに出力されます。
var vm = { value: ko.observable('initial value') }; vm.value.subscribe(function(newValue){ console.log(newValue); });
この subscribe メソッドは戻り値として subscription オブジェクトを返します。この subscription の dispose メソッドを呼び出すと、イベントの購読を停止することができます。
var subscription = vm.value.subscribe(function(newValue){ console.log(newValue); }); subscription.dispose(); // イベントの購読を停止
依存性追跡を回避して値を取り出す
peek メソッドを呼び出すと、依存性追跡を行わずに observable の現在の値を取り出すことができます。これは主に computed observbale の評価関数内で使用する observable を依存させたくない場合に利用します。依存性追跡については、Computed Observable の項目で紹介します。
そのほか高度なメソッド
以上で紹介した他にも、特殊な場合を除き使うことは無いですが次のようなメソッドがあります。
- notifySubscribers
呼び出すと、observable からイベントを発火することができます。第一引数にイベントで通知する値、第二引数にイベント名を指定できます。
- valueHasMutated
呼び出すと、change イベントを発火することができます。
- valueWillMutate
呼び出すと、beforeChange イベントを発火することができます。
いずれのメソッドも、observable の値を更新した時に内部で呼び出されているメソッドです。直接呼び出すことで、その observable を購読しているサブスクリプション関数に対して変更を通知することができます。
ObservableArray
コレクション(配列)の変更を検知して View に変更を通知させたい場合は observableArray を使用します。なお、observableArray が変更を検知できるのは配列自身の状態なので、配列に格納された個々の値やオブジェクトのプロパティ対する変更を検知したい場合は Todo アプリの例の done プロパティのように observable と組み合わせて使う必要があります。
observableArray を作成するには ko.observableArray を呼び出します。呼び出し時に引数に与えることで初期状態を設定することができます。バージョン2.3.0 からは、配列以外のオブジェクトも引数に指定できるようになりました。初期値を指定しなかった場合の状態は空の配列になります。
ObservableArray のメソッド
ObservableArray では、配列の標準的なメソッドに加えて独自のメソッドも提供されています。これらのメソッドを呼び出して状態を変更することで、状態を監視している要素に変更が通知され UI が自動更新されます。
- push
- pop
- unshift
- shift
- sort
- reverse
- splice
- slice
独自のメソッド
- indexOf(item)
引数で指定した要素のインデックスを返します。要素が observableArray 中に無かった場合には -1 を返します。
- replace(oldItem, newItem)
引数で指定した oldItem に一致した配列内の要素のうち最初に見つかったものを newItem で置き換えます。oldItem が見つからなかった場合には何も行われません。
- remove(valueOrPredicate)
引数で指定したオブジェクトに一致する配列内の要素をすべて取り除き、それらを配列で返します。また、引数には関数を指定することで特定の条件を満たすような要素をすべて取り除くことができます。
var array = ko.observableArray([1, 2, 3, 4, 5, 6]); array.remove(function(n){ return n % 2 === 0;}); // 偶数の要素を取り除く console.log(array); // [1, 3, 5]
- removeAll(arrayOfValues)
引数で指定された配列の要素と一致する配列内の要素をすべて取り除きます。また、引数を指定せずに呼び出すと observableArray を空にすることができます。
var array = ko.observableArray([1, 2, 3, 4, 5, 6]); array.removeAll([2, 4, 6]); // 2, 4, 6 を取り除く console.log(array); // [1, 3, 5]
配列を取り出す
引数を指定せずに observableArray を関数として呼び出すことで、 observableArray が内部の状態を保持するのに使用している配列を取り出すことができます。
var array = ko.observableArray(); array.push(1); array.push(2); array.push(3); var innerArray = array(); console.log(innerArray); // [1, 2, 3]
observableArray も observable と同じように、取り出した配列に対して直接 push や pop を呼び出して変更を加えると、Knockout はその変更を検知できないので注意してください。ただし、配列に対して直接変更を加えた場合でも明示的に valueWillMutate や valueHasMutated を呼び出すことで変更を通知することができます。これを活用することで、observableArray のイベントの発火回数を抑えるようなことが実現出来ます。
var array = ko.observableArray(); var innerArray = array(); innerArray.push(1); innerArray.push(2); innerArray.push(3); array.valueHasMutated(); // 最後に change イベントを発火
Computed Observable
Computed Observable を使用することで、他の observable や observableArray に依存する observable を生成することができます。Computed Observable を生成するには内部の値を生成する評価関数を引数に指定して ko.computed メソッドを呼び出します。第二引数には評価関数内で使用する this に束縛したい値を指定することができます。Computed Observable は、評価関数内で値を取り出した observable の状態を監視し、その状態が変更された場合には自身の状態も更新します。
例えば次のような ViewModel の場合、fullName は firstName と lastName を結合した文字列を返す observable になります。 firstName もしくは lastName のどちらかの状態が変更された場合、fullName の状態も自動的に更新されます。
function ViewModel() { this.firstName = ko.observable(''); this.lastName = ko.observable(''); this.fullName = ko.computed(function () { return this.firstName() + ' ' + this.lastName(); }, this); }
Computed Observable から値を取得する
Computed Observable から値を取得するには、observable の場合と同じく引数を指定せずに observable を呼び出します。
依存性追跡を回避する
observable の項で少し紹介しましたが、peek メソッドを使用することで依存性追跡を回避して observable の現在の値を取り出すことができます。例えば次のような computed observable の場合、firstName の状態が変更されると fullName は自動的に更新されますが、 lastName の状態が変更された場合には更新されません。
this.fullName = ko.computed(function () { return this.firstName() + ' ' + this.lastName.peek(); }, this)
Computed Observable に値を書き込む
評価関数を引数に指定して生成した Computed Observable は読み込み専用となるため、他の observable のように直接状態を変更することはできません。Computed Observable に対して値を書き込んで直接状態を変更したい場合は読み込み用の関数と、書き込み用の関数をプロパティに持つオブジェクトを引数に指定します。
以下の例では、fullName を更新すると firstName と lastName の状態を更新することができます。
function ViewModel() { this.firstName = ko.observable(''); this.lastName = ko.observable(''); this.fullName = ko.computed({ read: function () { return this.firstName() + ' ' + this.lastName(); }, write: function(newValue){ var splited = newValue.split(' '); this.firstName(splited[0] || ''); this.firstName(splited[1] || ''); }, owner: this }); }
この機能を使うことで、次のような observable を作成することができます。
- 書き込み関数にバリデーション処理を設定して、適切な値のみを状態として保持できる observable
- 読み込み関数にフォーマット処理を設定して、フォーマットされた値を読み出せる observable
以上、今回は Observable についての解説でした。次回は、Observable の拡張機能について解説する予定です。おたのしみに。