ObservableArray

モジュールパス: /alier_sys/ObservableArray.js

概要

ObservableArray は配列操作の同期を行うためのオブジェクトです。基本的な振る舞いは ObservableObject の配列と同様ですが、それに加えて配列要素の削除や挿入、並べ替えに対する同期処理を行います。一方で ObservableArray は、通常の配列のように複数の型のデータを要素として混在させることはできず、各要素は共通のオブジェクト型に対する ObservableObject に制限されます。

ObservableArray とデータ同期をする対象は bindData() 関数によって指定できます。

observableArray.bindData(bindingTarget);

データ同期をする対象は、オブジェクトであり、syncComponents(operation) 関数および onDataBinding(source) 関数を実装している必要があります。

  • syncComponents(operation)
    • 同期が必要となった際に呼び出される関数です
    • 引数 operation は同期処理のための差分情報を持ったオブジェクトです
      • 引数 operation の内容に応じてバインディングターゲット側は適切にソース側のデータを反映させる必要があります
  • onDataBinding(source)
    • bindData() によってデータ同期をする対象となった際に呼び出される関数です
    • 引数 sourcebindData() のレシーバである ObserableArray です
      • 引数 source が持つ ObservableArray への参照の設定や、source に対する型検査などは onDataBinding() 側で実装する必要があります

コンストラクタ


バインディングソースとなるオブジェクトを生成します。

構文

new ObservableArray(archetype, options) => ObservableArray

引数

  • archetype: object

    配列要素のコピー元となるプレーンオブジェクトです。このオブジェクトは ObservableArray への要素の追加が行われるたびコピーされます。

  • options: object?

    オプション引数のオブジェクトです。

    • options.twoWay: boolean

      バインディングターゲットとの双方向の同期を許可するか否かです。true なら双方向の同期を許可し、false ならソースからの同期のみを許可します。

      • 既定では true が設定されます(双方向の同期が許可されます)。

      この値はメンバ変数 twoWay として参照できます。

    • options.initCount: number

      生成時点で用意される配列要素の個数です。

//  ObservableArray の初期化
const observableArray = new ObservableArray({ x: 42, y: "foo" }, { twoWay: true, initCount: 4 });

console.log(observableArray.twoWay);
//  ==> true
console.log(observableArray.length);
//  ==> 4
console.log(observableArray[0].x);
//  ==> 42
console.log(observableArray[0].y);
//  ==> "foo"
console.log([...observableArray].map(x => x.curateValues()));
//  ==> [ { x: 42, y: "foo" }, ..., { x: 42, y: "foo" } ]

詳細

ObservableArray を生成します。

引数 options.twoWay の値は生成されたオブジェクトの同名のプロパティ twoWay の値として使用されます。options.twoWaytrue ならバインディングターゲットとの双方向のデータ同期が許されます。options.twoWayfalse の場合、単方向のデータ同期のみが許され、バインディングソースからバインディングターゲットへのみ同期処理が行われます(バインディングターゲット側で生じた変更は無視されるか上書きされ、変更の反映は保証されません)。

引数 options.initCount の値は ObservableArray 生成時に用意される ObservableObject の個数です。指定されないか負の値を取った場合、ObservableArray は空の状態で初期化されます。

メソッド


bindData()

  • 引数で渡されたオブジェクトを、対象の ObservableArray のバインディングターゲットとして登録します。バインディングターゲットとなるオブジェクトは、対象の ObservableArray との間で配列操作および配列要素の変更が同期されます。

unbind()

  • 指定されたバインディングターゲットとの同期を終えます。

syncComponents()

  • バンディングターゲットとの同期処理を行います。

onDataBinding()

  • データ同期の対象となった際に呼び出されるコールバック関数です。

sort()

  • 対象の ObservableArray の要素の順序を並べ替えます。

splice()

  • 対象の ObservableArray の指定の位置から要素を削除し、また新たな要素を挿入します。

[Symbol.iterator]()

  • 対象の ObservableArray の配列要素に対して反復処理を行う反復可能オブジェクトを返します。この関数は values() 関数と同じ効果を持ちます。

values()

  • 対象の ObservableArray の配列要素に対して反復処理を行う反復可能オブジェクトを返します。この関数は [Symbol.iterator]() 関数と同じ効果を持ちます。

keys()

  • 対象の ObservableArray の配列要素の添え字に対して反復処理を行う反復可能オブジェクトを返します。

entries()

  • 対象の ObservableArray の配列要素の添え字と値の組に対して反復処理を行う反復可能オブジェクトを返します。

at()

  • 対象の ObservableArray から指定の位置の要素を取得します。

reduce()

  • 対象の ObservableArray先頭の要素から末尾の要素まで順に反復処理を行い、何らかの値1つを返します。

reduceRight()

  • 対象の ObservableArray末尾の要素から先頭の要素まで順に反復処理を行い、何らかの値1つを返します。

map()

  • 対象の ObservableArray の先頭の要素から末尾の要素まで順に反復処理を行い、各反復処理の結果を要素に持つ配列を返します。

filter()

  • 対象の ObservableArray の先頭の要素から末尾の要素まで順に反復処理を行い、条件に合致する要素を抽出します。

every()

  • 対象の ObservableArray の先頭の要素から末尾の要素まで順に反復処理を行い、すべての要素が条件に合致するかどうか検査します。

some()

  • 対象の ObservableArray の先頭の要素から末尾の要素まで順に反復処理を行い、いずれかの要素が条件に合致するかどうか検査します。

reverse()

  • 対象の ObservableArray の要素の順序を逆転します。

push()

  • 対象の ObservableArray末尾に指定した個数の要素を挿入します。

unshift()

  • 対象の ObservableArray先頭に指定した個数の要素を挿入します。

pop()

  • 対象の ObservableArray末尾の要素を削除して取り出します。

shift()

  • 対象の ObservableArray先頭の要素を削除して取り出します。

isBound()

  • 対象のオブジェクトがいずれかの ObservableArray と同期されているかどうかを検査します。

sourceOf()

  • 対象のオブジェクトのバインディングソースである ObservableArray を取得します。

プロパティ


twoWay

info
  • このプロパティは読み取り専用です。

バインディングターゲットとの双方向のデータ同期を許すかどうかを表す boolean です。true なら双方向の同期を許し、false ならバインディングソースからバインディングターゲットへの単方向の同期のみ許します。

boolean

//  ObservableObject のバインディングターゲットの簡易な実装
const BindingTarget = class {
    curateValues() {
        const valueMap = Object.create(null);

        for (const k in this) {
            if (!Object.prototype.hasOwnProperty.call(this, k)) { continue; }
            valueMap[k] = this[k];
        }

        return valueMap;
    }

    reflectValues(valueMap) {
        const updatedValues = Object.create(null);
        const currentValues = this.curateValues();

        for (const k in currentValues) {
            if (!Object.prototype.hasOwnProperty.call(currentValues, k)) { continue; }
            if (!(k in valueMap)) { continue; }
            const candidate    = valueMap[k];
            const currentValue = currentValues[k];
            if (!Number.isNaN(candidate) && candidate !== currentValue) {
                this[k]          = candidate;
                updatedValues[k] = candidate;
            }
        }

        const source = this.source;
        if (typeof source?.reflectValues === "function") {
            let updated = false;
            for (const _ in updatedValues) {
                updated = true;
                break;
            }
            return updated ? source.reflectValues(updatedValues) : updatedValues;
        } else {
            return updatedValues;
        }
    }

    onDataBinding(source) {
        if (typeof source?.reflectValues === "function" && typeof source?.curateValues === "function") {
            this.source = source;
            this.reflectValues(source.curateValues());
        }
    }
};

//  ObservableArray のバインディングターゲットの簡易な実装
const BindingTargetArray = class extends Array {
    #ctor;

    constructor(ctor) {
        super();
        this.#ctor = ctor;
    }

    get(index) {
        return this.at(index);
    }

    splice(startIndex, deleteCount, ...insertedItems) {
        const mappedItems  = insertedItems.map(x => new this.#ctor(x));
        const removedItems = super.splice(startIndex, deleteCount, ...mappedItems);
        this.syncComponents({
            from         : this,
            kind         : ObservableArray.OperationKind.SPLICE,
            startIndex   : startIndex,
            deleteCount  : deleteCount,
            insertedItems: mappedItems
        });
        return removedItems;
    }

    sort(compare) {
        const compare_ = typeof compare === "function" ? compare : (x, y) => ((x > y) - (x < y));
        const indexMap = [];
        const valuesWithIndices = [...this.entries()];
        valuesWithIndices.sort(([, x], [, y]) => compare_(x, y));
        for (const [to, [from, fromValue]] of valuesWithIndices.entries()) {
            if (from < to) {
                indexMap.push({ from, to });
                this[from] = this[to];
                this[to]   = fromValue;
            }
        }
        this.syncComponents({
            from    : this,
            kind    : ObservableArray.OperationKind.SORT,
            indexMap: indexMap
        });
        return this;
    }

    syncComponents(operation) {
        if (operation == null || this.source == null) { return; }
        if (operation.from === this) {
            this.source.syncComponents(operation);
        } else {
            switch (operation.kind) {
                case ObservableArray.OperationKind.SORT: {
                    const indexMap = operation.indexMap;
                    for (const { from , to } of indexMap) {
                        const fromValue = this[from];
                        this[from] = this[to];
                        this[to]   = fromValue;
                    }
                    break;
                }
                case ObservableArray.OperationKind.SPLICE: {
                    const { startIndex, deleteCount, insertedItems } = operation;
                    const mappedItems = insertedItems.map(x => new this.#ctor(x.curateValues()));
                    super.splice(startIndex, deleteCount, ...mappedItems);
                    break;
                }
                default:
                    break;
            }
        }
    }

    onDataBinding(source) {
        if (typeof source?.syncComponents !== "function") { return; }
        Object.defineProperties(this, {
            source: {
                configurable: true,
                writable    : false,
                enumerable  : false,
                value       : source
            }
        });
    }
};

//  BindingTarget を継承するユーザ定義のデータクラス
const MyData = class extends BindingTarget {
    constructor({x, y}) {
        super();
        this.x = x;
        this.y = y;
    }
};
const MyArray = class extends BindingTargetArray {
    constructor() {
        super(MyData);
    }
};

const oneWayArray = new ObservableArray({ x: 42 }, { twoWay: false, initCount: 4 });
const twoWayArray = new ObservableArray({ y: "foo" }, { twoWay: true, initCount: 4 });

const targetArray1 = new MyArray();
const targetArray2 = new MyArray();

console.log(oneWayArray.twoWay);
//  ==> false
console.log(twoWayArray.twoWay);
//  ==> true 

//  bindData で oneWayArray に対して双方向バインディングを要求する
const isTarget1Bound = oneWayArray.bindData(targetArray1, true);
console.log(isTarget1Bound);
//  ==> false; oneWayArray は双方向バインディングを許さない
console.log(targetArray2.source != null);
//  ===> false; targetArray1 に source が定義されていない(onDataBinding() が呼び出されていない)

//  bindData で twoWayArray に対して双方向バインディングを要求する
const isTarget2Bound = twoWayArray.bindData(targetArray2, true);
console.log(isTarget2Bound);
//  ==> true;  twoWayArray は双方向バインディングを許す
console.log(targetArray2.source != null);
//  ===> true; targetArray2 に source が定義されている(onDataBinding() が呼び出されている)

length

配列の長さを表す整数です。

数値を代入した場合、代入した数値に長さが等しくなるよう、配列要素の削除または挿入を行います。

number