三者三様のイベントハンドリングを掛け合わせて究極のハンドリングパターンを実現する
from @miyamonz
概要
上記記事で同じマウスイベントのハンドラを3人が別々に書いた
yuki_minoh.icontakker.iconmiyamonz.icon
そこで「いいとこ取りできるのでは」と考えたyuki_minoh.iconがコードを書きます
yuki_minoh.iconの感想
yuki_minoh.iconの書いたコードは、エラーハンドラが書きやすい代わりに見通しが少々悪い
miyamonz.iconさんのコードはコールバックやthenチェーンをほとんど意識する必要がないため抜群に見通しがいいが、mouseMoveイベントでのみ関数クロージャを利用している
まあ関数クロージャ書くくらい別にいいんじゃね?と思ってますmiyamonz.icon
それはそうyuki_minoh.icon
後で、クロージャを解除したことでエラーハンドリングが簡潔になることに気づきましたyuki_minoh.icon
MouseMoveのハンドリング中のエラーもまとめてキャッチできるようになった
どうせmousemove eventですし、ここだけcallbackでも大してややこしくはならないでしょうtakker.icon
まあでもどうせなら全部同期的に書きたい感がある
takker.iconさんのコードはfor文でmouseMoveイベントをキャッチできるという点がとてもかっこいいと感じた。一方で、for文のネストが不必要に深いようにも見えた
listen関数はthenチェーンでより簡潔になるのではというアイデアも込み
考え直したんですけど、わざわざfor await(const event of listenEvent()) {...}にする利点がなさそうですtakker.icon
普通にwhile (true) {const event = await waitEvent(); ...}で同じことを十分再現できる
もちろんただの非同期関数だと一度しかresolveできないので、/miyamonz/usePromiseResolveのような機構で何度もresolveできるようにする
listenEvent()のなかで↑をやっていて、二度手間に感じる
非同期ジェネレータだと、rejectを外に出して中断させることができないという欠点がある
reject外に出す必要とは。。。?yuki_minoh.icon
えっと、中断機構は僕の方にも組み込んであります三者三様のイベントハンドリングを掛け合わせて究極のハンドリングパターンを実現する#616b64946668720000678441 よね?
abort(異常系)とclose(正常系)両方対応してます
あ、ほんとだすみませんtakker.icon
code readingが雑すぎました……
いくら雑に書く場所としてscrapboxを使えるとはいえど、ちゃんと読まないで共同の場で適当なことを書くのは避けるべきでした。今後気をつけます
いえいえ、気にしてはないので、今後もぜひのびのび意見交換できればいいなと思いますyuki_minoh.icon
rejectは何となく外に出してみただけです。それ以上の理由はありません(適当すぎる)takker.icon
もともとrejectを使うつもりはなかった/takker/dragできるcomponentをasync-await loopで実現#616cc9b91280f000002aabf6 のですが、なんとなく外に出していたrejectを使えばfor awaitを抜け出せることに気づいてやってみただけです
このあとrejectを使わない方法/takker/dragできるcomponentをasync-await loopで実現#616cb7931280f00000566c90 も思いつきました
という視点で試しに作ってみたのがこちら:
/takker/dragできるcomponentをasync-await loopで実現
おっしゃりたいことはよく分かりますが、僕が以下で実装したハンドリングとは関連性がかなり薄くなってきてますねyuki_minoh.icon
forループかそうでないかはあまり重要でない、というのはそうです
僕の場合だと、forループへの対応そのものにはほとんどコードの変更はいらないです
本質的にはasync function*宣言に変更して再帰呼び出しするだけです
forではなく、closePromiseへの対応には6行程度必要ですが、かなり簡潔に済んだのでそれも大きな違いではないかなと感じます
ほとんどコードの変更がないので、async generatorで便利に使えていいなと思ってます
rejectを外に出す必要は、僕の場合はもはやないかな、と感じています
むしろ出さないことでabortとcloseの両方に同時に対応する時とかにも困らない形になっているなと感じます
コードも読みましたが、reactを使うかどうかとかでかなり流儀?が異なるかもなと感じました
僕はライブラリ使わないでどこまでシンプルさと利便性を両立できるのかを突き詰めたかったので下記みたいな感じになってますね
僕のコードと比較した時、どういう見通しの良さとか利便性とかの違いがあるのかまでは比較しきれなかったので、何かあれば教えて欲しいです!
preactを使ったのは単にreact hooksを使ったコードを書きたかっただけなので気にしないでくださいtakker.icon
ただ下手にライブラリを使ってしまったことで、今回の処理と関係ない部分が増えて比較しにくくなったのは確かですね……
完全に自分だけがわかればいいのスタンスで書いてしまったのもよくなかったです
まぁまぁそういうこともありますよねyuki_minoh.icon
趣味コーディングなのはお互い様なので、気にしないでいいですよ
僕のコードへの意見も含まれてるのかなと思って、ちょっと困惑しただけです
後ほどnative DOMにのみ依存したコードを作っておきます
といっても/takker/getPromiseSettledAnytimesのresolveをaddEventListenerに登録して、ループでぐるぐる回すだけなのでほとんど同じですが
なんとなく似てる気はしてたyuki_minoh.icon
そこで、
miyamonz.iconさんのコードを全体に使いつつ、takker.iconさんのfor-await-ofパターンをmouseMoveイベントに適用してinner functionの呼び出しをなくし、コールバックを(メインループから)完全になくしてしまおう
yuki_minoh.iconのエラーハンドリングのしやすさもなるべく同時に実現できるように書いてみよう
実装内容
今回はマウスイベントを扱います。
マウスを押してからドラッグし、マウスを離すまでの処理を実装します。
コードと解説
今回の実装はPromiseベースですyuki_minoh.icon
なんかお料理のレシピみたいになってしまいましたw
下準備
まずイベントリスナをPromiseでラップします
全体の調理
下準備のおかげでとても良い焼き上がり(見通しの良い処理)になります
実食
実際に出来栄え(使い勝手)を確認してみます
下準備
code:handlePattern.js
// single event
function mouseUpPromise() {
return new Promise((resolve) =>
window.addEventListener("mouseup", resolve)
);
}
// single event with canceller
function mouseDownPromise(abortPromise) {
return new Promise((resolve, reject) => {
window.addEventListener("mousedown", resolve);
abortPromise.then(reject, reject);
ここってabortPromise.then(reject).catch(reject);でも同じでしょうか?takker.icon
ほぼ同じですねyuki_minoh.icon
今回は当てはまらないのですが、then中のエラーをcatchするかどうかという微妙な挙動が変わります
abortPromise.finally(reject)でもいけるかな?
code:handlePattern.js
});
}
// sequential event
async function* mouseMoveChain(closePromise) {
const promise = new Promise((resolve, reject) => {
window.addEventListener("mousemove", resolve);
closePromise.then(reject, reject);
});
try {
yield await promise, null;
} catch (event) {
yield null, event;
return;
}
// recursive
yield* mouseMoveChain(closePromise);
}
ここで、MouseUp, MouseDown, MouseMoveの三種類のイベントのリスナーをPromiseでラップしました。yuki_minoh.icon
このラップを書くことで、後述の処理がとてもスッキリしたものになります
それぞれ、上から順に、
最もシンプルなパターン(MouseUp)
キャンセル機能を実装したパターン(MouseDown)
シンプルなパターンに引数を追加したものです
キャンセルしたい時にresolve or rejectするPromiseを引数に受け取ります
今回は大域脱出したいので単にawaitし、reject処理を呼び出し元に任せています
イベントハンドリングを正常終了する場合はMouseMoveのパターンで使っているような形になります
これらの両立も可能です
for文で処理したい時のパターン(MouseMove)
キャンセル機能を実装したパターンをasync generatorにしました
AsyncGeneratorを再帰呼び出ししています
loopではなく再帰で実装する手があったかtakker.icon
まず普通にpromiseをラップし、キャンセルされない間はその結果を返します
キャンセル機能を実装するときはコツがあります
キャンセルで全てのループを抜けたい時は、単にyield await promiseで良いです
rejectされた時の処理を呼び出し元に任せる、という意味になります
このイベントリスナを正常終了したい場合は、[normalEvent, closeEvent]の長さ2の配列を返却するようにします
通常は[normalEvent, null]を返します
キャンセルされた時は[null, closeEvent]を返します
こうすることで、for文で処理するときに最後のイベントも受け取ることができます
イベントの判別も楽になります
ここが今回の実装の本質的な部分です
全体の処理を書きますyuki_minoh.icon
code:handlePattern.js
async function handleMouseEvent(abortPromise) {
const mouseDown = await mouseDownPromise(abortPromise);
console.group("mouseEvent");
console.log({ mouseDown, time: mouseDown.timeStamp });
let mouseMove, mouseUp;
このlet要るんでしたっけ?yuki_minoh.icon
僕のテストだといらなかったですが、TSとかでは怒られるかな?
use strict;ありやTSなら確実に怒られますtakker.icon
そうでなかったとしても未定義の変数をいきなり使うのは個人的にはばかられますね……バグの温床になりそうですし
code:handlePattern.js
for await (
mouseMove, mouseUp of mouseMoveChain(mouseUpPromise())
) {
if (mouseUp) break;
console.log({ mouseMove, time: mouseMove?.timeStamp });
}
console.log({ mouseUp, time: mouseUp?.timeStamp });
console.groupEnd("mouseEvent");
return handleMouseEvent(abortPromise);
}
以上の下準備があったおかげで、イベントの全体を同期的に書くことができましたyuki_minoh.icon
まずマウスの押下を処理し、次に連続したドラッグイベントの処理をやって、最後にマウスから手を離した時の処理、というのがわかりますね!
このハンドル関数では、コールバックやthenチェーンなどの非同期ならではの処理を気にする必要はありません
実際に使ってみますyuki_minoh.icon
code:usage.js
// usage
const controller = new AbortController();
const aborter = new Promise((r) =>
controller.signal.addEventListener("abort", r)
);
function onAbort(e) {
console.log("aborted", e);
}
handleMouseEvent(aborter).catch(onAbort);
https://gyazo.com/2f80900de4fd667c2ac78cc3cba62311
うん!美味しい!yuki_minoh.icon
今回はAbortControllerをキャンセル管理に利用しました
Promiseにラップしてハンドラーに渡しています
各自でご利用の際は、rejectを外部にExposeしたPromiseを渡すなど、お好きな味付けに調整してください
エラーハンドラーも問題なく作動します
上記の例だとcontroller.abort()でキャンセルできます
まとめ
Recursive Async Generatorで、イベントの同期的ハンドリングもラクラクみたいな感じのコードだった
全体として、簡単な下ごしらえで見通しよく処理を書くことができたと言えるのではないでしょうかyuki_minoh.icon
キャンセル処理やエラーハンドリングも効率よく実装できたと思います
僕は大満足の出来栄えです
miyamonz.icontakker.iconのお二人ともありがとうございました!
issue
abort起因のエラーとそれ以外のエラーが区別できない
改めてこのコードの系譜を整理しておきます
yuki_minoh.icon(旧)から
thenチェーンや再帰呼び出しを駆使した処理のギミックを引き継いでいます
処理の中止や正常終了などの処理分岐を利用しました
クロージャを一気に減らし、可読性を向上させています
miyamonz.iconさんから
同期処理で一連のマウスイベントをまとめてしまおう、という全体設計を引き継いでいます
MouseMoveイベントのハンドリングで唯一利用していた関数クロージャを無くしたことで、MouseMoveイベントのハンドリング中に発生したエラーもまとめてキャッチできるようになっています
takker.iconさんから
for-await-ofで連続したイベントを同期処理に落とせるのは目から鱗でした
listen関数の発想は今回のRecursive Async Generatorにつながっています
実装されていなかった終了処理の実装を行いました
余談
今回は3種類のpromise wrappingを行いましたが、この三つはTargetEvent, CloseEvent, AbortEventの三つのPromiseを引数に受け取るAsyncGeneratorに容易に抽象化することが可能だと思います
返り値は[TargetEvent, CloseEvent]
abortEventはエラーを投げる
大規模化して多数のイベントの多くの組み合わせを利用する必要がある場合にご利用ください
あと/takker/Async Generatorを外部から中断する、今初めて読んだ
なるほどかなり研究が進んでいたのか
/takker/Async Generatorを外部から中断する#61667bf71280f00000bfb849
これは今回のを使えばそこそこ簡単にできそう
イベントをArrayに保管しておいて随時利用するようにすればいい
処理の流れは基本的に今回のパターンをそのまま使えるはず
/icons/hr.icon
ここもしかするとonAbortコールされても以下のループloop()から脱出しないね?yuki_minoh.icon
AbortController知らなかったmiyamonz.icon
fetchとかをキャンセルできるっぽい
この例はmousedownを検知するPromiseをしてるっぽい
でも実はPromiseを外側からresolveする要領でrejectもできるんですよね。
なのでAbortControllerはいらないかも?
確かにyuki_minoh.icon
ちょっと考えたら以下の利点から採用したみがある
API提供側の視点:事前にリスナや処理構造を決められる
/icons/hr.icon
yuki_minoh.icon
あまりにもJavaScript Specificな話題なので別プロジェクトに移すかもしれない
2021/10/17 03:41 これで完成とします
解説を付しました
2021/10/16 04:51 動くようにはなった
最後の返り値がおかしい
issue
for await ofループを抜けた後のMouseUp eventをどう渡すか
memo
デザインパターンとして綺麗にまとめたい
頼みの綱はやっぱAsyncIteratorかな
あ、そうだ、分割代入で綺麗にいきそう
2021/10/15 09:54 経過を載せました
やりたかったことは全部表現しました
issue
reject catch問題
そもそもthen/catchチェーンがasync/awaitと上手く噛み合ってないっぽい
memo
あとでAsyncIterator拡張を試す
三者三様