immer が proxy を使っていることがわかったが、なぜproxy の仕組みが効率的に変更を追跡できるのかをもう少し調べてみた
1. Proxy の仕組み
Proxy は、JavaScript のネイティブ機能で、あるオブジェクトや関数の操作(プロパティの取得、設定、削除など)を「フック」する
つまり、操作が行われたときにカスタムの処理を差し込むことができる
const target = { name: "Alice" }; const proxy = new Proxy(target, { get(target, prop) { console.log(`Getting ${prop}`); return target[prop]; }, set(target, prop, value) { console.log(`Setting ${prop} to ${value}`); target[prop] = value; return true; // 必須: 成功したら true を返す }, }); console.log(proxy.name); // "Getting name" と出力、結果: "Alice" proxy.age = 25; // "Setting age to 25" と出力 console.log(target.age); // 25
2. immer における Proxy の役割
immer は元のオブジェクト(state)を Proxy でラップし、以下の操作を監視する
- プロパティの取得: 値を読み取る
- プロパティの設定: 値を書き換える
- ネストされたオブジェクトへの操作: 深い階層のオブジェクトの操作も追跡
例えば、次のコードを考える:
const state = { user: { name: "Alice", age: 25 } }; const newState = produce(state, (draft) => { draft.user.age = 26; });
このとき、以下のように動作する
初期状態
stateをProxyでラップして監視を開始変更の検知
draft.user.age = 26を実行すると、Proxyがこの「書き込み操作」をフックして、userオブジェクトが変更されたことを記録部分的な再生成
immerは「変更があった部分のみ」新しいコピーを生成する。ここではuserオブジェクトが変更されたため、userのコピーを作成し、変更を適用する全体の構造を維持
他の部分(たとえばstateのトップレベルオブジェクトや他のプロパティ)はコピーされず、元の参照がそのまま使用される
3. なぜ必要な部分だけ新しいオブジェクトが生成されるのか
Proxy を通じて「どのプロパティが読み取られ、どのプロパティが変更されたか」が完全に追跡されるため、次のような最適化が可能
変更の検出
どのプロパティが変更されたかをProxy内で記録遅延コピー
必要なタイミングでのみコピーを作成(「書き込みが発生したときだけコピー」)構造共有(Structural Sharing)
未変更部分は元のオブジェクトをそのまま再利用する。このため、パフォーマンスが非常に高い
4. immer の最適化ポイント
最小限のコピー作成
必要なときに必要な部分だけをコピーすることで、無駄なメモリ使用を削減構造共有の利用
未変更部分を再利用するため、大規模なオブジェクトでも効率が良い読みやすいコードの実現
Proxyの仕組みにより、ネストされたデータの操作を直感的に記述可能