TT Lab
はじめる
学ぶ 学習パス コース

クジラを探したのに、猫が現れた

キャンセルボタンでは時間旅行を止められない

TT Labで続きを見る

一言でいうと

もう必要のない作業をキャンセルすることと、遅れて届いた結果が画面を書き換えないようにすることは、別々の責任です。エフェクトのセットアップとクリーンアップを、1組として設計します。

なぜ必要なのか

クジラの検索を送る前に、猫のリクエストにabortを呼びました。これで安全でしょうか。ネットワークリクエストは止まっても、すでに終わったレスポンスの後処理が残っているかもしれませんし、使っている非同期ツールがキャンセルシグナルに対応していないかもしれません。相手がキャンセルを守ってくれるはずだという期待だけで画面の正確さを任せると、問題は別の場所に移ります。

この実験の配信モデルは、キャンセルシグナルを受け取ったという記録を残します。チェックボックスをオンにすると、そのあとでもレスポンスを配信できます。実際のHTTP実装だと偽っているのではなく、キャンセルと結果の無視を区別できるように、わざと制御したPromiseのモデルです。ネットワーク環境がたまたま遅くなるのを待たなくても、同じ順序を繰り返せます。

どう動くのか

リクエストAのためのエフェクトがセットアップされると、Aだけの有効期間が始まります。依存値が変わってリクエストBのエフェクトがセットアップされる前には、Aのクリーンアップが実行されます。コンポーネントが消えるときにも、クリーンアップが必要です。クリーンアップでは、2つの問いに答えます。

  1. このエフェクトが作った結果を、今後画面に反映してよいか。
  2. このエフェクトが始めた外部の作業を、これ以上実行する必要があるか。

最初の問いは、activeフラグやリクエスト識別子で処理できます。2つ目は、AbortControllerと、キャンセルシグナルに対応したツールで処理します。どの実装を選ぶにせよ、リクエスト同士で状態を誤って共有してはいけません。古いリクエストのクリーンアップが最新のリクエストをキャンセルしてしまうと、ユーザーは新しい検索をするほどレスポンスを失います。

成功の経路だけを守るのでは不十分です。Aが遅れて失敗し、Bがすでに成功していたなら、AのcatchがBの画面にエラーを表示してしまうことがあります。逆に、Bの最新の失敗のあとにAの成功が来ても、最新のエラーの説明が消えてはいけません。成功と失敗のどちらも、同じ有効性ポリシーに従う必要があります。

クリーンアップの方式 過去のレスポンスによる上書き 不要な作業のキャンセル
結果だけ無視 防げる 別途対応が必要
キャンセルだけ要求 ツールのキャンセル保証に依存 対応しているツールで可能
有効性の検査とキャンセル 2つの責任を明示的に処理 2つの責任を明示的に処理

空の検索語についても考えてみましょう。ユーザーが入力を消して提出したなら、新しい画面はidleです。その前に送ったレスポンスが届いて結果を復元すると、ユーザーの最後の意図に逆らいます。同じ単語を再び提出したときにも、提出番号が変われば再試行できなければなりません。

activeをどこに置くか

すべてのリクエストが共有する変数1つをactiveと呼ぶと、名前は合っていても寿命が間違うことがあります。Aをクリーンアップしてfalseに変えたあと、Bを開始して同じ変数をtrueに変えると、遅れて届いたAもtrueを読みます。Aの終了の印を、Bの開始が消してしまったことになります。各エフェクトのセットアップが自分の変数をキャプチャするようにするか、レスポンスが持ってきたリクエスト番号と現在のリクエスト番号を比較する方式が必要です。

この違いを、手で追跡してみましょう。Aのセットアップで作られたフラグを、Aのthenとcatchが読みます。Aのクリーンアップは、そのフラグだけをfalseに変えます。Bのセットアップは別のフラグを作り、Bのコールバックはそれを読みます。2つのコールバックが同じ名前の変数を使っていても、同じ保存領域であるわけではありません。コードの変数名よりも、宣言された位置と、クロージャがキャプチャした対象を見る必要があります。

リクエスト番号の方式にも落とし穴があります。現在の番号を増やさず、同じ検索語の文字列だけを比較すると、同じ単語の2回の提出を区別できません。1回目のリクエストの古い結果が、2回目の再試行の結果であるかのように入ってくることがあります。検査には、異なる単語の逆順のレスポンスだけでなく、同じ単語を連続して提出した場合も入れます。

クリーンアップの効果を確認するときは、レスポンスの完了をやみくもに待ちません。キャンセルシグナルは、リクエストが終わらなくても確認できます。過去のレスポンスをわざとあとで完了させて画面の保護を見て、別にabortedを読んでリソースの後始末を確認します。観察の対象を分ければ、どちらの責任が抜けているのかも、より正確に説明できます。

現場での姿

開発用のStrictModeでは、エフェクトのセットアップ・クリーンアップ・再セットアップを観察できます。最初のセットアップの作業がクリーンアップされないなら、2回目のセットアップと一緒に残ります。これを避けるためにクリーンアップの検査をなくすよりも、セットアップとクリーンアップを繰り返しても、必要な作業だけが生きているかを確認します。本番ビルドで、開発時の点検と同じ実行回数を前提にしてはいけません。

メモリリークの警告がないからといって、クリーンアップされているわけではありません。画面が消えたあとの状態更新が見えなくても、リクエストやイベントの購読は生き続けているかもしれません。検査では、DOMの結果だけでなく、渡したsignalのaborted状態も観察します。ユーザーがウィンドウを閉じるという出来事が、リソース使用の終わりとつながっている必要があります。

自分で確認すること

キャンセルを無視するモードで、成功と失敗のレスポンスをそれぞれ逆順に配信します。次に、キャンセルを守るモードで、検索画面を破棄します。結果が正しいことと、キャンセルシグナルが伝わったことをそれぞれ確認し、どちらか一方だけを実装した誤答が、どの検査で失敗するかを探してみましょう。

原理は、Reactのエフェクトのクリーンアップの説明を参照してください。この実験の手動配信モデルと出来事番号は、学習用の設計です。