この章では Angular の語彙としての「変更検知」について解説します。
変更検知とは何か
公式ドキュメントの用語集では次のように説明されています。
The mechanism by which the Angular framework synchronizes the state of the UI of an application with the state of the data. The change detector checks the current state of the data model whenever it runs, and maintains it as the previous state to compare on the next iteration.
Angular フレームワークがアプリケーションの UI の状態をデータの状態と同期させるメカニズム。 変更検知の実行時はいつも、データモデルの現在の状態をチェックし、それを次回の反復において比較するために前の状態として保持します。
As the application logic updates component data, values that are bound to DOM properties in the view can change. The change detector is responsible for updating the view to reflect the current data model. Similarly, the user can interact with the UI, causing events that change the state of the data model. These events can trigger change detection.
アプリケーションのロジックがコンポーネントデータを更新すると、ビュー上の DOM プロパティにバインドされた値が変更することがあります。 変更検知は、現在のデータモデルを反映してビューを更新する役割を果たします。 同様に、ユーザーは UI を操作して、データモデルの状態を変更するイベントを発生させることができます。 これらのイベントは変更検知をトリガーできます。
変更検知を理解するためには、まずは「変更」が指すものと、それを「検知」するということの意味を理解する必要があります。
変更とは何か
変更検知における変更とは、コンポーネントの状態の変化を指す。コンポーネントの状態とは、あるコンポーネントがビューを生成する際に、その入力となるコンテキストのことです。まずは次のサンプルコードを見てみよう。
このコンポーネントは message
というプロパティをもつ。このプロパティはテンプレートから参照され、データバインディングされている。
@Component({
template: `{{ message }}`,
})
export class SomeComponent {
message = 'Hello, world!';
}
このコンポーネント定義は、Angular のテンプレートコンパイラによって次のような関数の生成に使われ、この関数によって実際にビューがレンダリングされる。このテンプレート関数の引数に使われる ctx
こそが、このコンポーネントの状態を表すコンテキストであり、コンポーネントのインスタンスオブジェクトです。次の例ではテンプレートからコンテキストが直接的に参照されています。
function SomeComponent_Template(renderFlag: RenderFlags, ctx: SomeComponent) {
if (renderFlag & RenderFlags.Create) {
// 初回はテキストノードを生成
ɵɵtext(0);
}
if (renderFlag & RenderFlags.Update) {
// 生成済みのテキストノードの値を更新
ɵɵtextInterpolate(ctx.message);
}
}
ただし、コンポーネントの状態はテンプレートから参照されているだけでなく、テンプレートから参照されていないプロパティでも間接的にコンポーネントの状態とみなされる場合もあります。次の例を見てみましょう。
このコンポーネントは name
というプロパティを持ちます。このプロパティはテンプレートから参照されていません。しかし、テンプレートから参照されている message
ゲッターの内部では計算に使われています。したがって、この場合は name
プロパティもこのコンポーネントの状態だといえます。
@Component({
template: `{{ message }}`,
})
export class SomeComponent {
name = 'John';
get message() {
return `Hello, ${this.name}!`;
}
}
このように、コンポーネントの状態はテンプレートから参照されているだけでなく、テンプレートから参照されていないプロパティでも間接的にコンポーネントの状態として扱われるべき場合があります。それらをひっくるめて、コンポーネントの状態が変化したかどうかを知るのが、変更検知の目的です。
どのように変更を検知するか
基本的に、コンポーネントの状態はコンポーネントのインスタンスオブジェクトが保持するプロパティから構成されます。そうすると、その変更を検知するためにはどのような方法が考えられるでしょうか。
これは、単純化すれば JavaScript において 2 つのオブジェクトが異なる値を持っていることをどのように判定するかという問題に帰着します。もっともシンプルには、===
厳密比較演算子などによって 2 つのオブジェクトが同じかどうかを判定する方法が考えられます。ですが、コンポーネントの変更検知においては同じクラスインスタンスを対象にしているためこの方法は使えません。つまり、同じオブジェクトについて、そのプロパティに変更があったかどうかを判定する方法が必要です。
アプリケーションはコンポーネントの状態が変化したことをなるべく早く知り、最新の状態をビューに反映しなければなりません。ここには整合性と計算量のトレードオフがあります。どんなに小さな変更も見逃さないように変更検知をしようとすれば、実際には再レンダリングする必要のない場合にも反応してしまいます。一方、再レンダリングが必要となるであろうプロパティの変更を絞って頻度を減らせば、再レンダリングされるべきタイミングで再レンダリングされないおそれがあります。
Angular がデフォルトの変更検知戦略(Change Detection Strategy)としているのは、前者です。つまり、どのプロパティもテンプレートに影響しうるという前提で、検知対象のプロパティを絞り込むことはしません。さらに、変更があったかもしれないならば、実際に変更があったかどうかにかかわらず最新の状態で再レンダリングしなおすという戦略です。これはパフォーマンスの最適化よりも、コンポーネントの状態とビューの整合性が保たれることを重視しています。フレームワークの自動的なパフォーマンス最適化のためにアプリケーションの振る舞いが壊れるようなことはあってはならないからです。
当然、この戦略はパフォーマンスの観点からは不利です。ひとつひとつのコンポーネントやコンポーネントツリーが大きくなるほど、ビューの再レンダリングにかかる時間も大きくなります。パフォーマンスを最適化するためには、なるべく再レンダリングする回数と対象を減らさなければなりません。この問題を解決するために用意されているのが、もうひとつの変更検知戦略のOnPush Strategyです。
変更検知の最適化については、web.dev で公開されているOptimize Angular's change detectionが学習リソースとして適しています。ぜひそちらを参照してください。
まとめ
Angular の変更検知とは、ひとことで言えば「コンポーネントの状態が変化したことを検知する仕組み」です。アプリケーションが正常に動作するためには、コンポーネントの状態とビューの状態を常に同期させなければなりません。そのためには、状態が変化したことを検知し、ビューを再レンダリングしなおす必要があります。そのための仕組みが変更検知です。
Angular がデフォルトの変更検知戦略として提供しているのは、アプリケーションとしての整合性を優先した戦略です。つまり、変更があったかもしれないならば、実際に変更があったかどうかにかかわらず再レンダリングしなおすという戦略です。これはパフォーマンスを少なからず犠牲にし、大規模なアプリケーションになればその影響が無視できなくなります。そのため、開発者はパフォーマンスの悪化が大きな問題になる前から、変更検知の最適化を行う必要があります。