この記事の目次
案内パネルを開き直すたび、ボタン一回で増える数字が大きくなる。処理の計算式より、初期化のたびにイベントを追加していないかを調べます。イベント登録の重複を防ぐには、登録した処理をいつ終わらせるかも一緒に決めます。
JavaScriptの関数とaddEventListenerを使ったことがある人向けの練習です。架空の貸出道具を選ぶパネルを題材に、同じDOM要素を繰り返し表示します。購入や予約は行わず、画面上の選択回数だけを数えます。Reactのイベント指定とは分けて、標準のDOM APIで扱います。
一回のクリックで、何本の関数が動いているか
初期化のたびにbutton.addEventListener('click', () => count++)を実行すると、そのたびに新しい関数が作られます。ソースの文字が同じでも、別々に生成された関数です。前の登録が残ったまま追加すると、一回の操作から複数の処理が呼ばれます。
MDNのaddEventListenerの説明では、同じ対象・種類・関数・captureの組を繰り返し登録した場合と、別々の無名関数を渡した場合を区別できます。「同じ行だから二回目は無視される」とは考えないでください。
まず、再表示を三回行ってから一回押す、という順序で現象を記録します。不具合の再現手順と同じく、読み込み直後に戻す条件が必要です。調査の途中でページを再読み込みすると登録も消えるため、問題が直ったように見える場合があります。
パネルを開く前に、前回の登録を終える
今回の単位は「パネルを一度開いてから閉じるまで」です。その間の登録を一つのAbortControllerへ結び付け、閉じるときにabortで解除します。AbortControllerは処理の中止を伝える仕組みで、ここでは通信ではなくイベントリスナーの解除に使います。
新しく開く場合は、前回の後始末を先に実行します。これにより、すでに開いているときに「パネルを開く」をもう一度押しても、登録が増えません。単に閉じるボタンを用意するだけではなく、初期化を繰り返しても一組に保つのが方針です。
次をpanel.htmlへ保存して開きます。追加のライブラリは不要です。JavaScriptが動くまで開くボタンを無効にし、閉じた後は開くボタンへフォーカスを戻します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>選択パネルの登録と解除|練習</title>
<style>
body { margin: 1rem; font-family: sans-serif; line-height: 1.8; }
main { max-width: 36rem; margin: auto; }
button { font: inherit; margin: 4px; }
section { border: 2px solid #555; padding: 1rem; margin-block: 1rem; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style>
</head>
<body>
<main>
<h1>貸出道具の選択(架空の練習)</h1>
<p>数字はこのページでの選択回数です。予約は行いません。</p>
<button id="open" type="button" disabled>パネルを開く</button>
<section id="panel" hidden aria-label="道具の選択">
<button id="add" type="button">三脚を選ぶ</button>
<button id="close" type="button">パネルを閉じる</button>
</section>
<p id="result" role="status">選択回数:0</p>
</main>
<script>
const openButton = document.querySelector('#open');
const panel = document.querySelector('#panel');
const addButton = document.querySelector('#add');
const closeButton = document.querySelector('#close');
const result = document.querySelector('#result');
let count = 0;
let dispose = () => {};
function showPanel() {
dispose();
const controller = new AbortController();
panel.hidden = false;
addButton.addEventListener('click', () => {
count += 1;
result.textContent = `選択回数:${count}`;
}, { signal: controller.signal });
dispose = () => {
controller.abort();
panel.hidden = true;
};
addButton.focus();
}
openButton.addEventListener('click', showPanel);
closeButton.addEventListener('click', () => {
dispose();
openButton.focus();
});
openButton.disabled = false;
</script>
</body>
</html>
disposeには、現在のパネルの後始末をする関数が入ります。最初は何もしない関数にしておけば、初回も同じ順序で呼べます。新しい登録が終わった時点で、そのcontrollerを覚えた関数へ入れ替えます。
開くボタンと閉じるボタンはページの寿命中ずっと残るので、一度だけ登録します。繰り返し追加する対象は選択ボタンの処理だけです。全部のイベントを同じように何度も登録する必要はありません。クリック元と登録先の記事が扱う参照先の違いとは、別の点検項目です。
解除用に同じ形の関数を書く失敗
一つ目の失敗は、登録時に書いた無名関数と同じ文字列をremoveEventListenerへ書くことです。見た目は同じでも、解除用に作った関数は別物なので、元の登録は残ります。クリック回数が増える症状を直せません。
修正方法は二通りです。登録した関数そのものを変数で保持して解除へ渡すか、今回のようにsignalへ結び付けてabortします。removeEventListenerの資料では、対象、イベントの種類、関数、captureの一致が重要です。passiveなどのオプションをすべて同一に書き直すこととは違います。
完成例を使う場合は、閉じて再び開く操作を三回行ってから一回選びます。初期表示が0なら結果が1になり、3や4へ飛ばないことを再確認します。処理の回数を割って表示だけ合わせる修正では、登録自体が残るので解決になりません。
onceと、使い終えたsignalを取り違えない
二つ目の失敗は、重複対策としてonceを付けることです。onceは一度実行したリスナーを解除しますが、開いている間に何度でも選べる今回の仕様では、二回目の操作が反応しなくなります。修正ではonceを外し、パネルを閉じる時点で解除します。連続二回の操作で0から1、2と増えることを見ます。
三つ目は、controllerをページの先頭で一度だけ作り、閉じた後にも使い回すことです。abort済みのsignalは元に戻りません。次に開いたパネルへそのsignalで登録しても、期待する処理を継続できません。修正ではshowPanelの中で毎回新しいcontrollerを作り、閉じた後の再表示でも一回につき1だけ増えるか確認します。
登録を消しても、すでに始まった非同期処理まで自動で取り消されるわけではありません。たとえばハンドラー内で別に始めた通信は、その通信にも対応する中止の仕組みが必要です。ここでは同期的な数値更新だけを行い、解除の意味を広げすぎないようにしています。
部品の寿命に合わせ、所有する登録だけを片付ける
ページを一度開いて一度初期化するだけなら、読み込み順の確認のような構成で足りる場合もあります。再表示、タブの切り替え、部品の差し替えなど、初期化を何度も行う場面では後始末を呼ぶ場所を明確にします。
他の機能が登録したイベントをまとめて消そうとはせず、自分の部品が所有する登録を解除します。この例のcontrollerも、選択ボタンの一つの処理だけを管理しています。複数のイベントを同じsignalへ結び付ける場合は、本当に同時に終わる処理かを先に確認します。
開き直す順番で、1ずつ増えるか試す
15分程度を目安に、再読み込みして0へ戻し、パネルを開いて二回選びます。2になったら閉じ、開き直して一回選ぶと3になるのが期待結果です。次に「パネルを開く」を閉じずに三回操作し、その後一回選ぶと4になります。初期化を繰り返しても、一回の選択は一回分です。
閉じた後はTabで隠れた選択ボタンへ入らないこと、フォーカスが開くボタンへ戻ること、キーボードのEnterでも一回ずつ増えることを確認します。数字の一致と操作の復帰を別々に記録してください。JavaScriptの無料レッスンで部品を増やす際も、「登録する場所」と「解除する場所」を対で説明できれば、この練習を応用できます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。