e.currentTargetはイベントリスナーを追加した要素で、e.targetはイベントが発生した要素である。"current"と付くせいで、一見すると名称を逆にした方が直感的に思える。というか私にはそう思え、いつも盲滅法に使って間違える。なんとかして矯正したい。

というわけで由来を調べてみたら、意外とあっさり誤解を上書きできる解釈が見つかった。私の理解とは順番が逆だったのだ。currentTargetに"current"とつく理由は、イベントの方が先にあって、イベントリスナーを要素に後付けするからだ。

イベント伝播

Webページ上では、さまざまなイベントが発生する。これらのイベントの大部分は、その影響範囲が流動的である。HTMLは入れ子によってテキストを構造化するからだ。

例えばクリックイベントは、ボタンで捕まえたいケースもあればドキュメント全体で捕まえたいケースもある。これらを包括的に処理できる仕組みが求められる。

要素が入れ子になっているのだから、イベントが発生した末端の要素から親の要素に伝えていけば、すべての要素でイベントを捕まえることができる。これは、そのままイベント伝播=Event Propagationという名称で概念化された(「でんぱん」じゃないよ「でんぱ」だよ⚡️)。

イベントプロパゲーションは、大きく3つの過程からなる。キャプチャリングフェーズとターゲットフェーズ、バブリングフェーズである。

キャプチャリングフェーズ

ルート要素から発火要素へと降っているフェーズを指す。発火したイベントを捕まえにいくイメージから(たぶん)、この過程はイベントキャプチャー(あるいはイベントキャプチャリング)と呼ばれる。

もしルート要素と発火要素の間に、キャプチャリングフェーズを対象としたイベントリスナーが指定されていれば、そのリスナーにはバブリングフェーズのイベントオブジェクトが渡される。

addEventListenerは、デフォルトでは後述のバブリングフェーズ、あるいはターゲットフェーズを対象にイベントリスナーを追加する。キャプチャリングフェーズでイベントを捕捉するには、第3引数のオプションオブジェクトに{ capture: true }が指定されている必要がある。

ターゲットフェーズ

文字通りイベントが発火した時点に到達したフェーズを指す。発火元の要素にイベントリスナーが追加されていれば、そのイベントリスナーにはターゲットフェーズのイベントオブジェクトが渡される。

バブリングフェーズ

発火要素からルート要素へと遡っているフェーズを指す。泡のようにイベント情報が浮き上がっていくことから、この過程はイベントバブリングと表現される。

もしイベントが発火した要素とルート要素の間にある要素にイベントリスナーが指定されていれば、そのリスナーにはバブリングフェーズのイベントオブジェクトが渡される。

すべてのイベントがバブリングするわけではない。focusイベントなど、境界がはっきりしているイベントはバブリングしない。ただ、このイベントバブリングは、DOMのイベントハンドリングを理解する上で非常に重要な概念である。

イベント伝播を確かめるサンプル

以下は、イベント伝播の挙動を確かめる簡単なサンプルである。「Click me」ボタンを押下すると、イベントリスナーの追加順ではなく、先に説明したフェーズの順に各イベントリスナーが実行されていることがわかるかと思う。

js
const button = document.createElement('button');
button.textContent = 'Click me';
const container = document.createElement('div');
container.appendChild(button);
document.body.appendChild(container);

const result = document.createElement('pre');
document.body.appendChild(result);

const EventPhase = {
  1: 'CAPTURING_PHASE',
  2: 'AT_TARGET',
  3: 'BUBBLING_PHASE',
};

const addResult = (e) =>
  (result.textContent += `
eventPhase:${EventPhase[e.eventPhase]}
target:${e.target.tagName}
currentTarget:${e.currentTarget.tagName}

----
`);

const addHandler = (dom) => {
  dom.addEventListener('click', (e) => addResult(e));
  dom.addEventListener('click', (e) => addResult(e), { capture: true });
};

addHandler(button);
addHandler(container);
addHandler(document.body);

currentTargetとtarget

そのページの要素にイベントリスナーが登録されていようといまいと、ブラウザは3つのフェーズに沿ってイベントを発火する。イベントのtargetは、もちろんイベントが発火した要素である。

addEventListenerによって要素にイベントリスナーが追加されると、その要素はイベントが伝播している過程の"現在"の要素として捉えられる。そのためイベントのcurrentTargetは、イベントリスナーが追加された要素である。

これでもう間違えないぞ。

参考資料