画面更新時にイベントを消失してしまう原因

# はじめに
先日下記の事象について苦戦し、自分なりの結論までたどり着いたので共有します。

現在携わらせていただいている業務システムの UX を改善するため、JavaScript イベントによる .NET メソッドの呼び出しを実装していた際、表題の事象が発生しました。
UX の改善についてですが、今回はとあるデータの更新処理を行う際、現行システムではボタン押下を契機としていましたが、ボタンの押し忘れによる更新漏れが見受けられたため、ユーザーが最後にキーを押した時点からの経過時間を契機として、半強制的に更新処理を呼び出すよう改修したいと考えました。

# 事象
苦戦したのは、setTimeout 関数によってｎミリ秒間待機している最中に、別操作による画面遷移やブラウザの再読み込みが発生すると、コールバック関数が失われてしまうという事象についてです。

# 原因
画面遷移や再読み込みなどブラウザが更新される際 HTTP 通信が発生しますが、HTTPプロトコルの持つステートレス性という性質（状態を保持しない）が起因して、リクエスト前のブラウザの状態をレスポンス時には失っていたことが原因だと考えられます。

## JavaScript について
JavaScript とはシングルスレッドでノンブロッキングかつ、非同期処理や並行処理が可能なスクリプト言語です。
※ V8 などモダンな JavaScript エンジンでは、実行効率や速度を向上させるため、インタプリタを使用せず JIT（実行時）コンパイラによって処理されています。

## JavaScript ランタイム（実行環境）
次に JavaScript ランタイムが提供する以下の主要なコンポーネントについて説明します。

#### JavaScript エンジン

+ **コードの変換**
JIT コンパイラによって、JavaScript コードをマシンコードへ変換します。

+ **メモリ領域の割り当て**
以下のルールに沿ってメモリ領域の割り当てをします。
  + スタック領域
関数やオブジェクトの**参照**や**プリミティブ型**はコンパイル時に必要なメモリサイズが決定するため静的にメモリ領域の割り当てが行われます。

  + ヒープ領域
関数やオブジェクトの**実体**は実行時に必要なメモリサイズが決定するため動的にメモリ領域の割り当てが行われます。

+ **コールスタック**
実行中の関数や関数の階層状態を記録している LIFO 方式のデータ構造で、関数は呼び出されるとコールスタックへ Push され、処理を終えるとコールスタックから Pop されます。

#### Web APIs
ブラウザが提供する Web API とその実行環境のことです。コールスタックから非同期関数等が呼び出されると、対象の API を実行し、引数として受け取ったコールバック関数を、条件を満たすまで Web API Container で保持します。API の条件を満たした場合、Web API Container で保持していたコールバック関数を、コールバックキューへ Push します。

#### コールバックキュー
非同期関数の処理を終えたコールバック関数を保持しておく FIFO 方式の領域です。

#### イベントループ
コールスタックが空になる度に、コールバックキューで保持しているコールバック関数をコールスタックへ追加します。


## JavaScript 実行処理フロー
ブラウザで JavaScript が実行される際は以下の流れで処理が進みます。

1. ブラウザで読み込まれた JavaScript コードが JIT コンパイラによってマシンコードに変換され、データの種類に応じたメモリ領域の割り当てを行う。

2. 呼び出された関数はコールスタックへ Push される。例えば関数Aで関数Bが呼び出された場合、同様にコールスタックへ Push される。

3. 非同期関数が呼び出された場合、ブラウザが提供している Web APIs から対象の API を呼び出し、引数として渡したコールバック関数は Web API Container へ登録され条件を満たすまで待機する。

4. 待機中のコールバック関数は API の条件を満たすと、コールバックキューへ Push される。

5. コールスタックで処理を完了した関数はコールスタックから Pop される。

6. コールスタックが空の場合、コールバックキューで待機しているコールバック関数は、イベントループによってコールスタックへ Push される。


### サンプルプログラム

```qiita.cshtml
<div>@Html.EditorFor(model => model.ITEM, new { htmlAttributes = new { @onkeyup = "return keyUp();" } } )</div>
<input type="submit" id="btn" formmethod="post" formaction="/controller/action" style="display:none;" />
```

```qiita.js
/*BeginFormなどは省略しています。*/
function setTimer() {
    var timer;
    
    function keyUp() {
        clearTimeout(timer);
        timer = setTimeout(updateData, n);
    }
    
    function updateData() {
        document.getElementById("btn").click();
    }
}
```

上記のサンプルプログラムでは下記のように処理が流れていきます。
1. ユーザの操作によってフォームに値が入力（削除）される
2. JavaScript の関数 keyUp( ) が呼び出される
3. 関数 keyUp( ) によって非同期関数 clearTimeout( ) が呼び出される
4. 関数 keyUp( ) によって非同期関数 setTimeout( ) が呼び出される
5. n ミリ秒待機後にコールバック関数 updateData( ) が呼び出される
6. HTML の input 要素の Click イベントが発火する

※今回の事象は上記の５の最中にブラウザが更新されることによるイベント消失です。


## サンプルプログラムの JavaScript ランタイム状態遷移
**初期状態**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/fc939512-4b67-5c97-bb35-690d7b7e20a0.png)

**フォームに値が入力（削除）され、JavaScript の関数 keyUp( ) が呼び出される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/2a6a5602-5774-653a-35fe-93e7d0c3ba58.png)

**関数 keyUp( ) により非同期関数 clearTimeout( ) が呼び出される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/41507884-d4aa-d81d-0878-f8b56b4856a3.png)

**関数 keyUp( ) により非同期関数 setTimeout( ) が呼び出される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/69f1398a-bef6-e4a4-ae99-ef0adec0a989.png)

**非同期関数 setTimeout( ) は Web APIs で実行され、
コールバック関数 updateData( ) は Web API Container で保持される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/be52f933-a7b0-1629-c685-2f45a8608f8a.png)

**n ミリ秒待機後にコールバック関数がコールバックキューに Push される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/ef6ee7e2-ff63-928d-880e-020fa77cf96f.png)

**イベントループにより空のコールスタックにコールバック関数 updateData( ) が Push される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/a2f0a6d1-0e32-58a7-6cfe-f44a11b3816c.png)

**関数 updateData( ) によりHTML の input 要素の Click イベントが発火する**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370160/fc939512-4b67-5c97-bb35-690d7b7e20a0.png)


**以上が上記サンプルプログラム実行時のJavaScript実行環境内の状態遷移です。**

 

# 結論

今回の事象に対して JavaScript の実行処理フローから想定できるイベント消失の原因は、ブラウザの更新時にコールスタックや Web API Container、コールバックキューを引き継げていなかったことだと考えられます。

そこで、beforeunload イベント（ブラウザ更新の際に現在表示しているページがアンロードされる直前に発生する）を契機に、バックグラウンドで更新処理をしてしまう方法も考えましたが、ユーザーが知らないうちに意図せず更新されてしまうリスクを考慮し断念しました。

結果的に操作自由度を制限し、発生しうる操作ミスに対応する方式で決着をつけました。
→更新データ入力用のモーダルウィンドウを設け、ウィンドウを閉じる際の入力値から更新状態を判定し、更新漏れをアラートで警告する方式

# さいごに
今回の記事内容に対して至らない点や、より最適な実装方法がありましたら、ご指摘いただけると幸いです。

最後までお読みいただきありがとうございました。
また逢う日まで。

### 参考文献

https://engineering.mercari.com/blog/entry/20220128-3a0922eaa4/

https://developer.mozilla.org/ja/docs/Glossary/Call_stack

https://postd.cc/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code/