カテゴリー
開発・Web制作

再帰的に ko.track する

複雑な構造の ViewModel を knockout.es5 で扱う際、再帰的によろしくやってくれるものがないため作成してみました。

なにはともあれ、オブジェクトのプロパティが knockout.js の Observable になっているだけなのか、knockout-es5 の ko.track が当たってるのか分かりにくいので、デバッグ用の関数を作っておきます。

/**
 * プロパティが track されているか調べる
 * @param {Object} obj 調査するオブジェクト
 * @param {string} propName obj のプロパティ名
 * @returns {string[]|boolean} 配列1番目は ko または ko.es5。2番目は object か array。
 *                             何も仕込まれていない場合は false。
 */
function getKoPropStatus(obj, propName) {
    var propInfo = Object.getOwnPropertyDescriptor(obj, propName);
    // track されている場合は getter がいるはず
    if (typeof propInfo === 'object' && propInfo.get) {
        // getter が observable になっていれば ko.es5
        if (ko.isObservable(propInfo.get)) {
            if (isObservableArray(propInfo.get)) {
                return ['ko.es5', 'array'];
            }
            else {
                return ['ko.es5', 'object'];
            }
        }
    }
    if (ko.isObservable(obj[propName])) {
        if (isObservableArray(obj[propName])) {
            return ['ko', 'array'];
        }
        else {
            return ['ko', 'object'];
        }
    }
    return false;
}

/**
 * ObservableArray か調べる
 * @param {Object} obj 調査するオブジェクト
 */
function isObservableArray(obj) {
    if (obj.compareArrayOptions && obj.compareArrayOptions['sparse']) {
        return true;
    }
    return false;
}

複数の状態を配列で返却するのは甘えな気がしますが、デバッグ用ということで許していただきたいです。まずは ko.track の結果ですね。やはり第一引数で渡したオブジェクトのプロパティのみが対象です。その中身は見てくれません。

let data = {
    arr: [
        {
            'prop': '配列内の場合',
        },
    ],
    obj1: {
        obj2: {
            'prop': 'オブジェクトの入れ子の場合',
        }
    }
};

ko.track(data); // ふつうに指定

console.log(getKoPropStatus(data, 'arr'));            // ["ko.es5", "array"]
console.log(getKoPropStatus(data.arr[0], 'prop'));    // false
console.log(getKoPropStatus(data, 'obj1'));           // ["ko.es5", "object"]
console.log(getKoPropStatus(data.obj1, 'obj2'));      // false
console.log(getKoPropStatus(data.obj1.obj2, 'prop')); // false

ついでなので、ko.track({deep: true}) についても調べてみましょう。残念ながら配列の中のオブジェクトは適用範囲外です。

let data = {
    arr: [
        {
            'prop': '配列内の場合',
        },
    ],
    obj1: {
        obj2: {
            'prop': 'オブジェクトの入れ子の場合',
        }
    }
};

ko.track(data, {deep: true}); // 第2引数にオプション指定

console.log(getKoPropStatus(data, 'arr'));            // ["ko.es5", "array"]
console.log(getKoPropStatus(data.arr[0], 'prop'));    // false
console.log(getKoPropStatus(data, 'obj1'));           // ["ko.es5", "object"]
console.log(getKoPropStatus(data.obj1, 'obj2'));      // ["ko.es5", "object"]
console.log(getKoPropStatus(data.obj1.obj2, 'prop')); // ["ko.es5", "object"]

大した内容でもないのに引っ張ってしまいましたが、再帰的に適用するコードを載せます。

/**
 * オブジェクトを再帰的に ko.track する
 * @param {Object} viewModel track したいオブジェクト
 * @returns 引数の viewModel をそのまま返す
 */
function trackAll(viewModel) {
    ko.track(viewModel);
    
    for (var key in viewModel) {
        if (Array.isArray(viewModel[key])) {
            trackArr(viewModel[key]);
        } else if (typeof viewModel[key] === 'object') {
            trackAll(viewModel[key]);
        }
    }

    return viewModel;
}

/**
 * 配列を ko.track する
 * @param {any[]} arr 
 */
function trackArr(arr) {
    for (var i = 0;i < arr.length;i++) {
        if (Array.isArray(arr[i])) {
            ko.track(arr[i]);
            trackArr(arr[i]);
        } else if (typeof arr[i] === 'object') {
            trackAll(arr[i]);
        }
    }
}

上を使用して試した結果が以下です。配列内も track してくれてます。

let data = {
    arr: [
        {
            'prop': '配列内の場合',
        },
    ],
    obj1: {
        obj2: {
            'prop': 'オブジェクトの入れ子の場合',
        }
    }
};

trackAll(data);

console.log(getKoPropStatus(data, 'arr'));            // ["ko.es5", "array"]
console.log(getKoPropStatus(data.arr[0], 'prop'));    // ["ko.es5", "object"]
console.log(getKoPropStatus(data, 'obj1'));           // ["ko.es5", "object"]
console.log(getKoPropStatus(data.obj1, 'obj2'));      // ["ko.es5", "object"]
console.log(getKoPropStatus(data.obj1.obj2, 'prop')); // ["ko.es5", "object"]

ひとまずはこれで可能な限り observable なプロパティにできるようになりました。次はこれを使用した上で、ViewModel の変更が View 側にも適切に反映されるか確認する必要がありそうです。

最後に、ko.mapping という普通の observable を再帰的に適用できるプラグインがあるので、こちらの挙動も見てみました。

let data = {
    arr: [
        {
            'prop': '配列内の場合',
        },
    ],
    obj1: {
        obj2: {
            'prop': 'オブジェクトの入れ子の場合',
        }
    }
};

let viewModel = ko.mapping.fromJS(data);

console.log(viewModel);

console.log(getKoPropStatus(viewModel, 'arr'));            // ["ko", "array"]
console.log(getKoPropStatus(viewModel.arr()[0], 'prop'));  // ["ko", "object"]
console.log(getKoPropStatus(viewModel, 'obj1'));           // false
console.log(getKoPropStatus(viewModel.obj1, 'obj2'));      // false
console.log(getKoPropStatus(viewModel.obj1.obj2, 'prop')); // ["ko", "object"]

こちらはこちらで、オブジェクトの入れ子は observable ではないですが、入れ子オブジェクト内の文字列が入っているプロパティは observable になるという結果に。何が正しいのか分からなくなってきますね。

全部 observable にすると速度が落ちそうなので、再帰的に変換するのはあきらめて ko.valueHasMutated(viewModel, ‘prop’) と組み合わせて使う方がよいのかもしれません。

コメントを残す

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

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