はじめに
- モダンなフロントエンドフレームワーク(Angular, Vue.js 等)で必ずと言っていいほどある データのバインディング機能
- JavaScript 側で値を持ち、それをHTMLに反映させる(また その逆の)機能ですね。
- モダンな Webページ を構築するための必須機能といえます。
- そんなバインディング機能を実現するにはどうすれば?という点が気になったので、調べたり試したりした記録を残します。
用意するもの
- Visual Studio Code
- Node.js ・・・ 動作確認のため(今回は値の変更検知を確認したいだけなので、ブラウザは不使用)
準備
- 適当なディレクトリを作成し ターミナルで
npm init -y
実行 - 生成された
package.json
を以下の通り編集(これでnpm start
を実行すると <実行したいJSファイル名> について動作確認ができます)
{
"name": "<ディレクトリ名>",
"version": "1.0.0",
"description": "",
"main": "<実行したいJSファイル名>",
"scripts": {
"start": "node ."
},
"keywords": [],
"author": "",
"license": "ISC"
}
調査
Object.defineProperty()
- 値の監視として使えそうな JavaScript の機能です。今回はこちらを使用してみます。
- なんと ES5 の頃から機能として存在している模様・・・今まで気づかなかった・・・
参考ドキュメント
- Object.defineProperty() - JavaScript | MDN
- 独自のゲッターおよびセッター Object.defineProperty() - JavaScript | MDN
要約
- 任意のオブジェクト(のインスタンス)に対して、プロパティを定義できます。
- 単純なプロパティ定義だけでなく、ゲッターやセッターの動作を定義したり、プロパティの書き込みを禁止したり… 等、挙動の定義もできます。
実装1:オブジェクトのプロパティを監視する(第一階層のみ)
- まずは特定のオブジェクトの第一階層にあるプロパティを監視してみます。
コード
/**
* 値の変更を監視します
* @param {Object} obj 監視対象のオブジェクト
* @param {String} propName 監視対象のプロパティ名
* @param {function(Object, Object)} func 値が変更された際に実行する関数
*/
function watchValue(obj, propName, func) {
let value = obj[propName];
Object.defineProperty(obj, propName, {
get: () => value,
set: newValue => {
const oldValue = value;
value = newValue;
func(oldValue, newValue);
},
configurable: true
});
}
// 適当なオブジェクトを定義
const obj = {
name: '太郎',
age: 18,
favorite: {
food: 'ice cream',
animal: 'cat'
}
};
// 変更時に実行したい関数を定義
function onChange(v1, v2) {
console.log(v1);
console.log(' =>', v2);
console.log('');
};
// オブジェクトの第一階層すべてのプロパティを監視する
Object.getOwnPropertyNames(obj).forEach(propName => watchValue(obj, propName, onChange));
// 実際に変更してみる
obj.name = '次郎';
obj.age = 100;
obj.favorite = { food: 'pizza' };
実行結果
太郎
=> 次郎
18
=> 100
{ food: 'ice cream', animal: 'cat' }
=> { food: 'pizza' }
- Node.js で実行してみると、無事に変更が検知され、コンソールに 変更前 => 変更後 の値が出力されています!
ポイント
-
defineProperty
の引数として、以下3つを指定します- プロパティを定義したいオブジェクト
- プロパティ名(文字列)
- デスクリプタ(プロパティの動作等を定義するオブジェクト)
- デスクリプタでは
set
,get
つまり セッターとゲッターを定義します。 - 汎用的に利用できるよう
watchValue()
関数を作りfunc
引数で受ける関数を値変更時に実行するようにしています(=値の監視)
実装2:オブジェクトのプロパティを監視する(プロパティ階層掘り下げ)
- 実装1のコードでは、例えば以下のように 第二階層以下のプロパティに直接アクセスして変更した場合には、検知できません。
obj.favorite.food = 'potato';
- そこで先程までのコードを改良し、渡されたオブジェクトのすべてのプロパティに対し、監視するよう修正してみます。
- 再帰的にオブジェクトを掘り下げ、すべてのプロパティを監視するようにします。
コード
/**
* 値の変更を監視します
* @param {Object} val 監視対象のオブジェクト
* @param {String} propName 監視対象のプロパティ名
* @param {function(Object, Object)} func 値が変更された際に実行する関数
*/
function watchValue(val, propName, func) {
let value = val[propName];
Object.defineProperty(val, propName, {
get: () => value,
set: newValue => {
const oldValue = value;
value = newValue;
func(oldValue, newValue);
},
configurable: true
});
}
/**
* 与えられたオブジェクトのプロパティを監視します
* @param {Object} obj 監視対象のオブジェクト
* @param {function(Object, Object)} func 値が変更された際に実行する関数
*/
function watchAll(obj, func) {
Object.getOwnPropertyNames(obj).forEach(propName => {
const val = obj[propName];
if ((val instanceof Object) && !Array.isArray(val)) {
// オブジェクトの場合
watchAll(val, func);
} else {
// その他の場合
watchValue(obj, propName, func);
}
});
}
// 適当なオブジェクトを定義
const obj = {
name: '太郎',
age: 18,
favorite: {
food: 'ice cream',
animal: 'cat'
}
};
// 変更時に実行したい関数を定義
function onChange(v1, v2) {
console.log(v1);
console.log(' =>', v2);
console.log('');
};
// 監視
watchAll(obj, onChange);
// 実際に変更してみる
obj.name = '次郎';
obj.age = 32;
obj.favorite.food = 'potato';
obj.favorite.animal = 'dog';
実行結果
太郎
=> 次郎
18
=> 32
ice cream
=> potato
cat
=> dog
- オブジェクトを掘り下げて 無事に変更検知できるようになりました!(問題点・後述)
問題点
- 無事に変更検知する仕組みが整ってめでたしめでたし・・・
- ・・・と言いたいのですが、まだ 配列 が残っています!
配列各要素の変更に対応できていない
-
push()
やpop()
などで配列の個々の要素に変更を加えた場合、上記の方法だけでは 各要素に対する変更は検知できません。 - 長くなったため詳細は 次回 へ・・・