結局、ko.track はどこまで効くのか


knockout.js のプラグインである knockout-es5 を使用すると、双方向バインディングが利用できるようになります。入力欄の内容が ViewModel に入るだけでなく、ViewModel に代入した値が画面に反映されます。

ただ、単純に ko.track(obj) するだけだと、ViewModel の深い階層のオブジェクトに値を代入しても画面に反映されなかったり、今まで動いていたのに配列を書き換えた途端に動かなくなったりと、いまいち挙動がつかめません。一通り以下で試してみようと思います。

基本

まずは基本から。文字列のプロパティです。これは問題ありません。
※ 実行環境は webpack を使用しています。HTML に js 読み込みの記述がないのは webpack がよろしくやってくれるためです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <main style="width: 15em;background-color: #ddd; border-radius: 0.5em;padding: 0.5em;">
        <div data-bind="text: div"></div>
    </main>
</body>
</html>
const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        div: 'bb',
    };
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.div = 'dd'; // 書き変わる
});
Chrome の Capture node screenshot が便利

配列要素の書き換え・追加

次は配列です。これも問題ありません。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <main style="width: 15em;background-color: #ddd; border-radius: 0.5em;padding: 0.5em;">
        <ul data-bind="foreach: arr">
            <li data-bind="text: $data"></li>
        </ul>
    </main>
</body>
</html>
const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            'value1',
        ],
    };
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.arr[0] = 'value1-alt'; // 置き換わる
    viewModel.arr.push('value2'); // 追加される
});
 

配列の置き換え

HTML は先ほどのを使いまわします。

const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            'value',
        ],
    }
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.arr = ['arr2-value1', 'arr2-value2']; // 動く!
    viewModel.arr[1] = 'arr2-value2-alt'; // 動かない!
});
さっきはできたのに!

ko.track を呼んだ段階で、配列の要素まではアップグレード(双方向バインド状態に変換)されます。しかし、ko.track を通していない配列を代入すると、配列の要素の双方向バインド状態が切れてしまっています。ならば ko.track すればいいじゃんということで、

const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            'value',
        ],
    }
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    let newArr = ['arr2-value1', 'arr2-value2'];
    ko.track(newArr); // 代入する前に ko.track を通す
    viewModel.arr = newArr;
    viewModel.arr[1] = 'arr2-value2-alt'; // 動く!
});

深い階層

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <main style="width: 15em;background-color: #ddd; border-radius: 0.5em;padding: 0.5em;">
        <ul data-bind="foreach: arr">
            <li data-bind="text: prop"></li>
        </ul>
    </main>
</body>
</html>
const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            {
                'prop': 'value',
            },
        ],
    }
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.arr[0].prop = 'value-alt'; // 動かない!
    viewModel.arr[0] = {
        prop: 'value-alt'
    }; // 動かない!
});

ko.track がアップグレードしてくれる範囲は、第一引数で指定したオブジェクトの各プロパティ(プロパティが配列の場合はその要素も含む)までのようです。それより深いものはアップグレードされません。

悪あがき

const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            {
                'prop': 'value',
            },
        ],
    }
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.arr[0] = {
        prop: 'value-alt'
    }; // 動かない!

    // 反映される(すごい頭悪い感じ)
    viewModel.arr = viewModel.arr;
});

配列は track されていますが、値の変更が view (HTML)側にうまく通知されていないようです。そもそも knockout.js のドキュメントに、配列のインデクサ([]使って値を取る方法)を使用した代入についての記述がないので仕方ない気もします。(.push とか .remove など、配列のメソッドを使った変更しか反映されない)

const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            {
                'prop': 'value',
            },
        ],
    }
    ko.track(viewModel);
    ko.applyBindings(viewModel, $('main')[0]);

    viewModel.arr[0].prop = 'value-alt'; // 動かない!

    // 反映されない!!
    viewModel.arr = viewModel.arr;
});

配列の要素は track されていますが、オブジェクトの中身は track されていないのでこのような挙動になります。

再帰的に反映する

先ほどのコードを書き換えて、配列の各要素にも ko.track を実行すると動くようになります。再帰的に探索して全オブジェクト、全配列を ko.track すれば不具合なく使えそうです。

const $ = require('jQuery');
const ko = require('knockout-es5');

$(function(){
    let viewModel = {
        arr: [
            {
                'prop': 'value',
            },
        ],
    }
    ko.track(viewModel);
    for (let i = 0;i < viewModel.arr.length;i++) {
        ko.track(viewModel.arr[i]);
    }
    ko.applyBindings(viewModel, $('main')[0]);
    
    viewModel.arr[0].prop = 'value-alt'; // 動く!
});

deep オプションが使えそうですが、配列の中はたどってくれません。プレーンなオブジェクトのみであればたどってくれます。自前で書くしかなさそうです。

ko.track(nestedObj, { deep: true });

まとめ

ko.track の適用範囲

  • ko.track に渡したオブジェクトのプロパティのみ(浅い階層のみ)
  • プロパティが配列の場合は、配列の値の変更も追跡してくれる(値がオブジェクト・配列の場合は例外、array[index] を使った代入は反映されない)

viewModel の深い階層までバインドしたい場合

  • applyBinding する前に再帰的に ko.track する
  • オブジェクト・配列を ViewModel へ代入する前に ko.track する

これが正しいなら再帰的な ko.track をライブラリ側で用意してくれそうなものですが、無いということは使い方が間違ってるような気がしてきます。viewmodel を書き換えるたびに要素を削除&再生成してバインドし直す方が確実かもしれません。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください