← 解説「変更検知 — 「いつ・どこを」再描画するかの世代交代」に戻る
サンプルコードで身につける: 変更検知 — 「いつ・どこを」再描画するかの世代交代
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1zone.js のパッチを覗く
「非同期APIを横取りする」が具体的に何なのかをコードで見ます。setTimeoutが起動時に別物へ差し替えられていて、コールバックの完了をAngularに通報していた、というのがzone.jsの正体です。
// zone.js が起動時にやっていること(概念)
const originalSetTimeout = window.setTimeout;
window.setTimeout = (callback, delay) => {
return originalSetTimeout(() => {
callback(); // 本来の処理を実行してから
zone.notify(); // 「何か終わった」とAngularへ通報
// → ApplicationRef.tick() → ルートから全査定
}, delay);
};
// クリック、Promise、XMLHttpRequest なども同様にパッチ。
// だから「setTimeout で普通のプロパティを書き換えただけでも
// 画面が更新されていた」— 便利だが、通報のたびに全査定が走る。
// polyfills の zone.js の1行が、この横取りの読み込みだった
2全査定とOnPushの査定範囲を図で比べる
同じクリック1回で、既定(全査定)とOnPushで実行される更新モードの範囲がどう違うかを木の図で比べます。v22でOnPushが既定になった意味が視覚的に分かります。
// Counter のボタンをクリックしたときに更新モードが走る範囲
//
// ── 既定(zone.js + 全査定)── ── OnPush ──
// App ✓査定 App ✓
// ├ Header ✓査定(変化なし) ├ Header スキップ
// ├ Sidebar ✓査定(変化なし) ├ Sidebar スキップ
// │ └ Menu ✓査定(変化なし) │ └ Menu (親ごと)
// └ Main ✓査定 └ Main ✓
// └ Counter ✓査定 ←ここだけ変化 └ Counter ✓ イベント発生元
//
// OnPush の枝が査定されるのは:
// ・input が新しい値になった
// ・枝の中でイベントが発生した
// ・枝の中で使っているシグナルが変わった
// のいずれかのときだけ。「変わりようがない枝は見ない」剪定
3シグナルの「読む=依存登録」を時系列で追う
標的更新が成立する鍵は、テンプレートが値を「読んだ」瞬間に依存が記録されることです。登録→変更→標的実行の3ステップを時系列で並べます。computedやeffectと同じ仕組みがビューにも効いています。
// count = signal(0) と {{ count() }} の裏側
//
// [初回描画時]
// CounterComponent の更新モードが count() を読む
// → シグナル側に「このビューが購読者」と記録される
// (computed が依存を自動追跡するのと同じ機構。
// ビュー自体が1つの「購読者」になっている)
//
// [count.set(1) の瞬間]
// → 購読者リストにあるビューへ dirty マークを付ける
// → スケジューラに「次の描画タイミングで処理」を予約
//
// [次の描画タイミング]
// → dirty なビューだけ更新モードを実行
// → 台帳比較で 0→1 の差分だけDOMへ反映
//
// zone.js が「いつ」を通報し損ねる心配も、全査定の無駄もない
4ゾーンレスで画面が止まる典型例と直し方
v22編で「普通のプロパティを外から書き換えると更新されない」と学びました。その内部理由がこのレッスンで説明できます。通報者(zone.js)がいなくなったので、変更を台帳に伝える手段がシグナルだけになったのです。
@Component({
selector: 'app-timer',
template: '<p>{{ seconds }}</p>', // 普通のプロパティ
})
export class TimerComponent {
seconds = 0;
constructor() {
setInterval(() => {
this.seconds++; // 値は変わるが…
// ゾーンレスでは誰も「変わった」と知らない:
// ・zone.js の通報 → もういない
// ・シグナルの依存記録 → ただのプロパティなので無い
// → dirty にならず、更新モードが走らず、画面は 0 のまま
}, 1000);
}
}
// 直し方: seconds = signal(0) にして seconds.update(n => n + 1)
// → set が購読者のビューを dirty にする経路が復活する
5NG0100 — 開発モードの二重査定が守っているもの
有名なExpressionChangedAfterItHasBeenCheckedErrorも変更検知の内部から説明できます。開発モードは査定を2回走らせ、1回目と2回目で値が食い違ったら「描画中に値を変えた」設計ミスとして知らせています。
// NG0100: ExpressionChangedAfterItHasBeenCheckedError
//
// 開発モードの変更検知は各ビューを2回査定する:
// 1回目: 通常の更新(台帳に値を記録)
// 2回目: 確認専用(値が1回目と同じであるべき)
// → 食い違ったらこのエラー
//
// 典型的な出し方: 査定の最中に値を変えてしまう
// ngAfterViewInit() {
// this.title = '変更!'; // 親の査定が終わった直後に書き換え
// }
//
// なぜ守るのか: 査定中に値が動くと「どこまで描画したら
// 終わりか」が収束しなくなるため。一方通行(状態→描画)を
// 保つための検問で、本番モードでは検問ごと省略される。
// シグナル+ゾーンレスでは「書き込み→次の描画タイミングで反映」
// が徹底されるため、この種の食い違い自体が起きにくい