OnInitializedAsync の途中でもレンダリングはされる
Blazor のコンポーネントにおけるライフサイクルメソッドのひとつとして、OnInitializedAsync
というメソッドがあります。この OnInitializedAsync
メソッドは、戻り値に Task
を返す async/await 構文で記述できる非同期メソッドで、コンポーネントの初期化のタイミングでいちど呼び出されます。同様のライフサイクルメソッドとして、その同期版である OnInitialized
というメソッドもあります。
OnInitializedAsync
をオーバーライドすることで、例えば以下のように、OnIntializedAsync
メソッド内で、REST API から初期データを取得、みたいなことを実現できます。
@inject HttpClient Http
@if (this._item is null) {
<p>初期化中です..</p>
}
else {
<p>初期化完了しました!</p>
}
@code {
private ToDoItem? _item;
// コンポーネントの初期化時に呼び出されます
protected override async Task OnInitializedAsync() {
_item = await this.Http.GetFromJsonAsync<ToDoItem>("./api/todos/1");
}
}
ここでちょっと面白いのは、この OnInitializedAsync
メソッドは、その中に記述したすべてのコードが実行されるまでレンダリングが待たされるのではなく、最初の await で Task の完了待ちになった時点で、いちどレンダリングが実行される、という点です。
実際、上記コード例でいえば、REST API からの応答が返るまでの間、ブラウザ画面には「初期化中です...」と表示されているのを見ることができます。
このことからも、OnInitializedAsync
メソッド内の処理が途中であっても、非同期処理の await のタイミングでいったんレンダリングはされている、ということがわかります。
OnInitializedAsync の途中でも OnAfterRender も呼ばれる
いったんレンダリングはされるので、レンダリング完了時のライフサイクルメソッド OnAfterRender
(ないしはその非同期版である OnAfterRenderAsync
) も、OnInitializedAsync
メソッド内の処理がすべて完了する前であっても呼び出されます (下記コード例参照)。
<!-- 省略 -->
@code {
private ToDoItem? _item;
protected override async Task OnInitializedAsync() {
_item = await this.Http.GetFromJsonAsync<ToDoItem>("./api/todos/1");
// 実行がここに到達する前に...
}
protected override void OnAfterRender(bool firstRender) {
// (REST API からの応答を待っている間に) 先にここが呼び出されます!
if (firstRender) {
Console.WriteLine("初回レンダリングが完了しました");
}
}
}
OnInitializedAsync
メソッドは一般に,コンポーネントの初期化処理を記述することが多いので、OnInitializedAsync
メソッド内のすべての処理が完了するまでは、初回のレンダリングは行なわれないものと、つい思ってしまうことがあるかもしれませんが、実はそうではない、 ということです。
メソッド名も、よくよく読むと、"On Initialized" と過去形になっており、より厳密に読み取れば「初期化が完了した」あとのタイミングで呼び出されるよ、言い方を変えると、そういう "通知" のメソッドに過ぎないよ、といったことが示唆されていると理解できます。
複数コンポーネントで OnInitializedAsync
が並列実行されているように見えることもある
とりわけ Blazor WebAssembly はシングルスレッドで動作しますが (Blazor Server であっても、レンダリングは単一スレッドで行なわれます)、非同期処理が絡むと、複数コンポーネント間において、あたかも OnInitializedAsync
が並列実行されているように見えることもあります。
例えば、とある 3 つのコンポーネント、Component1.razor
、Component2.razor
、Component3.razor
があるとして、それらコンポーネントの実装は、みな下記内容となっているとします。
<fieldset>
<legend>Component 1</legend>
<div>
@(_step switch {
4 => "OnInitializedAsync 完了しました!",
_ => $"OnInitializedAsync ステップ {_step}..."
})
</div>
<div class="@($"progress-bar step-{_step}")"></div>
</fieldset>
@code {
private int _step = 0;
protected override async Task OnInitializedAsync() {
_step = 1;
await Task.Delay(Random.Shared.Next(500, 1000));
_step = 2;
StateHasChanged();
await Task.Delay(Random.Shared.Next(500, 1000));
_step = 3;
StateHasChanged();
await Task.Delay(Random.Shared.Next(500, 1000));
_step = 4;
}
}
すなわち、いずれのコンポーネントも、OnInitializedAsync
ライフサイクルメソッド内で、ランダムな数百ミリ秒の非同期待機をはさみながら「ステップ 1」「ステップ 2」「ステップ 3」「完了しました!」と表示を切り替える実装となっています。これらコンポーネントすべてを貼り付けたページを用意し (下記実装例)、
<Component1 />
<Component2 />
<Component3 />
実行してブラウザで開いてみると、下図のように、3 つのコンポーネントにおける OnInitializedAsync
メソッドが、あたかも並行実行されているかのように見えます。
もちろん、マルチスレッドで動作しているわけではなく、あくまでも非同期処理の組み合わせによって、あるコンポーネントが Task.Delay()
の完了を await しているタイミングで、別のコンポーネントが Task.Delay()
が完了したので処理継続する、といった動作になっているだけです。あるタイミングにおいて、OnInitializedAsync
メソッド内の処理が実行されているコンポーネントは、常にひとつだけであり、それはつまり、他のコンポーネントが await で待機状態である間に交代で処理を継続しているだけに過ぎません。
まとめ
同期版の OnIitialized
ライフサイクルメソッドについては、その中の処理がすべて完了して返るまでは初回レンダリングされません。そのため、その非同期版である OnIitializedAsync
メソッドも、ついうっかり、同じように動作するものと思ってしまうことがあるかもしれません。しかしながら、実際はそうではなく、非同期処理の待機にはいるタイミングでひとまずレンダリングはされてしまいます。
また、非同期処理の待機中は、他のコンポーネントの処理が継続されます。そのため、一見、複数のコンポーネントの各種メソッドが同時に並行実行されているかのように見えてしまうこともあります。しかし実際には、非同期処理の待機と復帰のタイミングで交代しながら実行されているだけです。
ぱっと見た目の印象とは異なる振る舞いですので、つい勘違いしがちではあります。しかしここの認識を誤ったままですと、順列に処理されなければならない初期化処理が前後してしまう、などの不具合にもつながりかねません。気を付けておきたいところですね。