ko.computedとko.pureComputed

これはKnockoutJSアドベントカレンダー5日目の記事です。


KnockoutJS Advent Calendar 2014 - Qiita


依存性の追跡(トラッキング)は ko.observable / ko.observableArray と ko.computed の組み合わせで行います。

例えば、生まれた年(西暦)から現在の年齢を算出するのをKnockoutを使って書くと次のようになります。

    <p>Enter your birth year: <input data-bind='value: birthYear' maxlength="4"/></p>
    <p>You're <strong data-bind='text: age'></strong> years old.</p>

    <script>
      function ViewModel() {
        var self = this;
        self.birthYear = ko.observable();
        self.age = ko.computed(function(){
          var now = new Date();
          var age = now.getFullYear() - parseInt(self.birthYear());
          if(isNaN(age)) return 0;
          return age;
        });
      }
      ko.applyBindings(new ViewModel());
    </script>
  • (値が変わらなくても)常に通知するようにしたり、一定時間経ってから通知したりする

上記のサンプルにあるように ko.computed の中で呼ばれる ko.observable / ko.observableArray の値が変わると ko.computed が呼ばれます。

逆にいうと値が変わらないと通知されないのですが、これを常に通知するように簡単に拡張できます。

myViewModel.fullName = ko.pureComputed(function() {
    return myViewModel.firstName() + " " + myViewModel.lastName();
}).extend({ notify: 'always' });


上記のように computed に .extend({ notify: 'always'}) をつけるだけです。

この書き方は extender と呼ばれ、 notify 以外にも rateLimit extender があります。

これは通知されるまでに設定された時間分待ってから通知します。

これは KnockoutJS 3.1.0 で追加されたのでそれ以前のバージョンでは throttle という名前でした(若干動きが違いますが・・・)

また、この extender は簡単に拡張できるのでドキュメントや実際のプロジェクトとか見てみると参考になるかと思います。

ドキュメント
Knockout : Using extenders to augment observables

実際のプロジェクト
knockout.persist/knockout.persist.js at master · spoike/knockout.persist · GitHub



もし computed 内で呼び出している observable A と observable B のうち、

observable A が変更したときだけ computed を実行して、observable B が変更されても computed が実行されない場合はどうしたらよいでしょうか。

これをするには computed 内の observable B に peek() を最後に追加するだけで実現できます。



  • ko.pureComputed とは何か

ko.pureComputed は Knockout 3.2.0 で追加されました。

公式サイトによるとこの機能を使うことにより以下の点で改善があるようです。

1. メモリーリークを防ぐ

2. 計算のオーバーヘッドを防ぐ

これはHTMLに現在バインディングされていない(アクティブでない) pureComputed は実行されないということです。

公式ドキュメントのサンプルがよくできているのでこれを使って説明します。

http://knockoutjs.com/documentation/computed-pure.html

<div class="log" data-bind="text: computedLog"></div>
<!--ko if: step() == 0-->
    <p>First name: <input data-bind="textInput: firstName" /></p>
<!--/ko-->
<!--ko if: step() == 1-->
    <p>Last name: <input data-bind="textInput: lastName" /></p>
<!--/ko-->
<!--ko if: step() == 2-->
    <div>Prefix: <select data-bind="value: prefix, options: ['Mr.', 'Ms.','Mrs.','Dr.']"></select></div>
    <h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
<!--/ko-->
<p><button type="button" data-bind="click: next">Next</button></p>
function AppData() {
    this.firstName = ko.observable('John');
    this.lastName = ko.observable('Burns');
    this.prefix = ko.observable('Dr.');
    this.computedLog = ko.observable('Log: ');
    this.fullName = ko.pureComputed(function () {
        var value = this.prefix() + " " + this.firstName() + " " + this.lastName();
        // Normally, you should avoid writing to observables within a pure computed
        // observable (avoiding side effects). But this example is meant to demonstrate
        // its internal workings, and writing a log is a good way to do so.
        this.computedLog(this.computedLog.peek() + value + '; ');
        return value;
    }, this);
 
    this.step = ko.observable(0);
    this.next = function () {
        this.step(this.step() === 2 ? 0 : this.step()+1);
    };
};
ko.applyBindings(new AppData());


このサンプルでは表示するHTMLをNextボタンを押す度に次のステップになり、ステップ0〜2 の 3回に分けて表示しています。

ステップ0では First name の入力、ステップ1では Last name の入力、ステップ2では Prefix の入力と fullName の表示を行っています。

ここで注目すべきところは、fullName が ko.pureComputed で定義されていることです。

fullNameの中では First name, Last name, Prefix を呼び出しているにも関わらず、fullName が実行されるのは

fullNameが実際にバインドされる「ステップ2」の段階になったときです。

今まで使っていた ko.computed だとどのステップにも関わらず実行されていたものを、ちゃんと「今つかっているか」を判断し、計算してくれます。

ko.pureComputed はComponentsを使った大規模なSPAでは、特に必要になってくるため地味な機能に見えて大変重要な機能ではないのかと思っています。

実際にサンプルも今まで ko.computed で書いてあったところを ko.pureComputed で書き直していたりしているので、

これからは ko.computed よりも ko.pureComputed を使った方が良い気がします。


逆に ko.computed を使う場面としては公式ドキュメントには以下のように書いてあります。

Knockout : Pure computed observables


1. 複数の observable に影響があるコールバックを ko.computed で使う場合

ko.computed(function () {
    var cleanData = ko.toJS(this);
    myDataClient.update(cleanData);
}, this);

2. バインディングの init メソッドで要素を更新するために ko.computed を使う場合

ko.computed({
    read: function () {
        element.title = ko.unwrap(valueAccessor());
    },
    disposeWhenNodeIsRemoved: element
});


用途として基本的には pureComputedを使い、初期値やサーバサイドで値が変わったら常に実行したい場合は ko.computed を使う感じなのかなと思います。



明日は @yusuke_nozoe さんです!よろしくお願いします!