はじめに
こんにちは、「CYBIRD Advent Calendar 2025」24日目を担当する@cy-yamakenです。
23日目は@ny512さんの「読みやすく伝わりやすい文章を書いてみよう!」でした。
要点を押さえて文章を書く方法について自分も考えさせられる内容でした。
気になった方はぜひ見てみてください!
概要
普段、Vue.jsやReactを使っていると、当たり前のように変数の値を変えたら、画面(DOM)も勝手に変わるという恩恵を受けています。
いわゆるリアクティブなシステムです。
// Vueの例
this.message = "Hello";
これはJavaScriptの標準機能で実現されているロジックです。
今回は、フレームワークなどのライブラリを一切使わず、Vanilla JSの Proxy オブジェクトを使って、この「データバインディング」の仕組みをゼロから再現してみます。
「フレームワークが裏で何をしているのか」を知ることは、デバッグ力やアーキテクチャ設計力の向上に直結します。
1. そもそも Proxy とは?
ES6 (ES2015) から導入された、オブジェクトの基本操作(値の取得、代入など)に割り込んで、独自の処理を挟むことができる機能です。
簡単に言うと、オブジェクトへのアクセスの検問を作ることができます。
- 「値を取得しようとしたな? その前にログを出力してやる」
- 「値を代入しようとしたな? 代入ついでに画面も書き換えてやる」
ということが可能です。これがリアクティブシステムの正体です。
2. 実装:まずは「片方向バインディング」から
まずは、「データが変わったらDOMを書き換える」部分を作ってみましょう。
HTML
<div id="app">
<p>現在のメッセージ: <span id="output"></span></p>
</div>
JavaScript
// 1. 元となるデータ
const data = {
message: '初期値です'
};
// 2. DOM要素の取得
const outputEl = document.getElementById('output');
// 3. ハンドラ(検問)の定義
const handler = {
// setトラップ: プロパティに値が代入された時に発火する
set(target, key, value) {
console.log(`${key} が ${value} に変更されました`);
// 実際に値を更新する
target[key] = value;
// 値の変更を検知して、DOMを書き換える
if (key === 'message') {
outputEl.textContent = value;
}
return true; // 成功を表すtrueを返す必要がある
}
};
// 4. プロキシの作成
// dataオブジェクトをhandlerでラップする
const state = new Proxy(data, handler);
// --- 動作確認 ---
// 初期表示のために一度セットする(あるいは初期化関数を作る)
state.message = data.message;
// 3秒後に値を書き換えてみる
setTimeout(() => {
// ここでDOM操作(element.textContent = ...)は書いていない!
// 変数に代入しているだけ!
state.message = "魔法の正体はProxyでした!";
}, 3000);
解説
state.message = "..." と代入した瞬間、handler の set メソッドが自動的に呼び出されます。その中で outputEl.textContent = value を実行しているため、「代入=画面更新」というリアクティブな挙動が実現できました。
3. 実装:入力も同期する「双方向バインディング」へ
次に、inputタグの入力値もデータに反映させてみましょう。
HTML (inputを追加)
<div id="app">
<input type="text" id="input" placeholder="入力してください">
<p>現在のメッセージ: <span id="output"></span></p>
</div>
JavaScript
// --- 状態管理クラス(簡易Vue的なもの)を作成 ---
class ReactiveSystem {
constructor(initialData) {
this.listeners = {}; // 更新したいDOMのリスト
// データをProxy化する
this.state = new Proxy(initialData, {
set: (target, key, value) => {
// 値を更新
target[key] = value;
// そのキーを監視しているDOMがあれば更新する
if (this.listeners[key]) {
this.listeners[key].forEach(callback => callback(value));
}
return true;
}
});
}
// DOMとデータを紐付ける(バインド)
bind(key, outputElement, inputElement = null) {
// 1. 初期値を表示
outputElement.textContent = this.state[key];
// 2. データ変更時のコールバックを登録
if (!this.listeners[key]) this.listeners[key] = [];
this.listeners[key].push((newValue) => {
outputElement.textContent = newValue;
// input要素もあれば値を入れる(同期ズレ防止)
if (inputElement && inputElement.value !== newValue) {
inputElement.value = newValue;
}
});
// 3. input要素がある場合、入力イベントを検知してstateを更新
if (inputElement) {
inputElement.value = this.state[key]; // 初期値
inputElement.addEventListener('input', (e) => {
// ここで state に代入することで set トラップが発動し、
// 結果として outputElement も更新される
this.state[key] = e.target.value;
});
}
}
}
// --- メイン処理 ---
const app = new ReactiveSystem({
text: 'Hello World'
});
const outputEl = document.getElementById('output');
const inputEl = document.getElementById('input');
// 'text' というデータと、DOM要素を紐付ける
app.bind('text', outputEl, inputEl);
// コンソールで app.state.text = "変更" と打っても画面が変わります
解説:何が起きているか?
このコードのデータの流れは以下のとおりです。
-
ユーザーが入力:
inputイベント発火 -
データの更新:
this.state['text'] = 'あ'が実行される -
Proxyが検知:
setトラップが発動 -
DOMの更新: 登録されたコールバックが走り、
<span>の中身が書き換わる
これが、Vue.js (v-model) などの双方向バインディングの基本的な原理です。
まとめ
Vue 2.x までは Object.defineProperty という機能(ゲッター/セッター)が使われていましたが、Vue 3 からは今回紹介した Proxy オブジェクトを使ってリアクティブシステムが再構築されています。
Proxy を使うメリットは、配列の変更やプロパティの追加・削除も検知しやすいことです。
普段何気なく使っているフレームワークの「便利機能」も、こうして Vanilla JS で分解してみると、決してブラックボックスな処理をしているのではなく、JavaScriptの言語仕様に基づいたロジックであることが分かります。
「なぜ動くのか」を知っていると、フレームワークで不可解なバグに出会った時の調査アプローチが変わります。
気になった方は是非サンプルコードで試してみてください。
最後に
明日の「CYBIRD Advent Calendar 2025」は25日目を担当する@cy_ryosuke_zushiさんです。
お楽しみに!!