Hatena::Groupptech

ぷちてく RSSフィード

Archive
 
ProfileProfile

2017-03-02

同イベントのハンドラがネスト構造なコード

06:46

最初に断っておくと、ネストを積極的に使う理由はないので極力避けましょう。しかし読む場合はあるし、書かなければいけないこともあるでしょう。つらい。

外側のハンドラ末尾で再度イベントが発生、内側のハンドラが呼ばれる、という構造でハマったのを書いた。解決したので解説みたいなものを書いていく。

外側ハンドラ末尾で再発生してるイベントを見落とした結果、理解出来ない挙動に *見えていた* という話で、再現失敗してたところ、a-kuma3 さんがコメントしてくれて気付いた。

理解と再現出来た。ただ、自分の検証コードで再現再失敗してまたわからない挙動が出てきてしまった。ウケる。

言葉ではわかりづらいので実際の例見てもらった方が早い。a-kuma3 さんがコメント後、さらにハイクしてくれたのがミニマムでわかりやすいので引用。ハイクの仕様で全角スペースなところを半角変換したのでコピペ再現出来る。

onload = なのは元コードがそうだったから。

xhr = new XMLHttpRequest();
xhr.open('GET', location.href);
xhr.onload = () => {
  console.log('outer');
  xhr.onload = () => {
    console.log('inner');
  };
  xhr.open('GET', location.href);   // もう一回 open
  xhr.send();
};
xhr.send();
// -> outer
// -> inner
>|javascript|xhr = new XMLHttpReque... - JavaScript - a-kuma3 - はてなハイク

順序は、

  1. 外側のハンドラが登録されるが何もしない
  2. 最後の xhr.send();load イベント発生
  3. 外側のハンドラが動く
    • xhr.onload を内側のハンドラが上書きするが何もしない
  4. 外側のハンドラ末尾の xhr.send(); で再度 load イベント発生
  5. 上書きした内側のハンドラが動く

という流れ。イベントドリブン初心者は理解キツそう。

このように外側と同じく内側も同じ順に書くと、自然。

xhr = new XMLHttpRequest();
xhr.open('GET', location.href);
xhr.onload = () => {
  console.log('outer');
  xhr.open('GET', location.href);
  xhr.onload = () => {
    console.log('inner');
  };
  xhr.send();
};
xhr.send();

open() -> onload -> send()

open() -> onload -> send()

addEventListener() ならこんな感じ。ちょっと長くなる。

xhr = new XMLHttpRequest();
xhr.open('GET', location.href);

outer = () => {
  console.log('outer');
  xhr.removeEventListener('load', outer);
  xhr.open('GET', location.href);
  xhr.addEventListener('load', () => {
    console.log('inner');
  });
  xhr.send();
};

xhr.addEventListener('load', outer);
xhr.send();
// -> outer
// -> inner

removeEventListener() 無いとハンドラが無限ループするので注意。

ネストしない

大抵の場合、こういう感じっぽくフラットに書ければ混乱しづらい。

xhr = new XMLHttpRequest();
xhr.open('GET', location.href);
xhr.addEventListener('load', () => {
  console.log('I was outer');
  // ...
});
xhr.addEventListener('load', () => {
  console.log('I was inner');
  // ...
});
xhr.send();
// -> I was outer
// -> I was inner

しかしネストしてたということは、逐一 fetch しなければいけない状況があるはずで、これでは動かないどうしよう…。

といった背景も一因になって DeferredPromise 等色々生まれてるので適宜やっていきましょう。

再現再失敗

ハイクで返事貰う前、「XHR より click イベントがお手軽そう」と思ってこんなコードを書いたら違うカルマを背負った。

body = window.document.body;
body.onclick = () => {
  console.log('outer');
  body.onclick = () => {
    console.log('inner');
  };
  body.click();
};
body.click();
// -> outer

アイエエエ!?」.click() が マズイのか、キャプチャリング・バブリングとかフェーズの問題なのか、単純にコードが間違ってるのか。

すぐ時間が無くなってとりあえずあきらめた次第。


続き書いた。

a-kuma3a-kuma32017/03/03 09:53.dispatchEvent(MouseEvent) だと、内側も発火した >Firefox

const do_click = (e) => {
 const clickEvent = new MouseEvent("click", {
  view: window,
 });
 e.dispatchEvent(clickEvent);
};

body = window.document.body;
body.onclick = () => {
 console.log('outer');
 body.onclick = () => {
  console.log('inner');
 };
 do_click(body);

};
do_click(body);


.click() を setTimeout() でずらしても、発火した。

setTimeout(() => body.click(), 0)

unaristunarist2017/03/03 21:57ちなみに最初はインスタンス使いまわせないと思っていて、コピペしてインスタンスだけ変えた、つもりが変わってなくて使いまわせることに気づいた感じですね。
# でも気持ち悪いなーと思ってたら見事に勘違いされたので、ネタ以外では使わないようにしよう…

noromanbanoromanba2017/03/06 03:01> a-kuma3
検証 thx です。`.dispatchEvent(MouseEvent)` と timer 、確かに。Chromium 56.0.2924.76。

body = window.document.body;
body.onclick = () => {
console.log('outer');
body.onclick = () => {
console.log('inner');
};
settimeout(() => body.click(), 0);
};
body.click();

`dblclick` で動くわけでもないので、 ブラウザ側の tick/timer/job queu(e)ing あたりか .click() の仕様とか内部で気を効かせてそう。

noromanbanoromanba2017/03/06 03:13> unarist
> 使いまわせることに気づいた感じ
なんと…。小さいとライブラリの導入もアレなんで、もしやるなら標準の Promise.all Promise#then するのがいいのかなー。「フラット化どうよ」みたいな声もありますが、気にせずやっていきましょう

noromanbanoromanba2017/03/06 03:25body = window.document.body;
body.onclick = () => console.log('ouch!');
for (i = 0; i < 10; i+=1) {
body.click();
}
// -> (10) ouch!

えっ、ネストか


Copylight (c) noromanba 2012-2017