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()
によってデータ同期をする対象となった際に呼び出される関数です- 引数
source
はbindData()
のレシーバである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.twoWay
が true
ならバインディングターゲットとの双方向のデータ同期が許されます。options.twoWay
が false
の場合、単方向のデータ同期のみが許され、バインディングソースからバインディングターゲットへのみ同期処理が行われます(バインディングターゲット側で生じた変更は無視されるか上書きされ、変更の反映は保証されません)。
引数 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
- このプロパティは読み取り専用です。
バインディングターゲットとの双方向のデータ同期を許すかどうかを表す 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