Panda Noir

JavaScript の限界を究めるブログでした。最近は幅広めに書いてます。

複数イベントを組み合わせたオリジナルイベントをカンタンにつくる方法

たとえば「これらのラジオボタンが全てクリックされたあとに発火する」みたいなイベント、普通に作ろうとするとフラグを管理するなどしなければなりません。しかし、これをカンタンにする方法を見つけたのでご紹介します。

Promiseとイベントハンドラを組み合わせる

ところで「非同期処理」といわれるとみなさんは何を思い浮かべますか?「ファイル読み込み」「HTTP通信」「Canvas描画」いろいろあります。しかし、もっと身近なものとしては「DOMイベント」があります。

いざ実装しようとすると実は面倒なことがあったりします(といっても少しですが)。

// 悪い例
new Promise(resolve => {$('#button1').on('click', resolve);});

// 良い例
new Promise(resolve => {$('#button1').one('click', resolve);});

(jQueryで解説しますが、ふつうにdocument.querySelectorを使って直でやってもいいです。ここでは面倒なのでjQueryでやります)

jQueryでonでイベントハンドラを設定してしまうと、その後もずっとresolveが実行されてしまいます。これが一つならいいですが、増えていくとパフォーマンスに悪影響を与える恐れがあります。そこで、一回発火したらイベントハンドラを外すoneメソッドを使うことをオススメします。

Promise.allと$.fn.oneを組み合わせる

Promiseには「Promise.all」や「Promise.race」といったメソッドが備わっています。これらは「与えられたPromiseがすべて完了したら完了とする」「与えられたPromiseのうちいずれか一つが完了したら完了とする」という機能です。つまり、さきほどの「これらのボタンが全てクリックされたら発火」というのはPromise.allを使えば実現できそうです。

というわけで実際に組んでみました。

jQuery($ => {
    const click = el => new Promise(resolve => {
        $(el).one('click', resolve); // 要素がクリックされたらresolveする
    });
    (async () => {
        await Promise.all([
            click('#button1'), click('#button2')
        ]); // #button1 と #button2 がクリックされる(=resolveされる)まで待つ
        console.log('all buttons clicked!');
    })();
    (async () => {
        await Promise.all([
            click('#button3'), click('#button4')
        ]); // 非同期だからコッチも同時に待つことが出来る
        console.log('button3 and button4 were clicked!');
    })();
});

ちなみに、ふつうに地獄のようにゴリ押しで実装するとこうなってしまいます。

jQuery($ => {
    {
        let flag = 0;
        const ONE = 1, TWO = 2;
        $('#button1').one('click', () => flag = flag | ONE);
        $('#button2').one('click', () => flag = flag | TWO);
        $('#button1,#button2').on('click', () => {
            if (flag === ONE & TWO) {
                // something...
            }
        })
    }
    {
        let flag = 0;
        const ONE = 1, TWO = 2;
        $('#button3').one('click', () => flag = flag | ONE);
        $('#button4').one('click', () => flag = flag | TWO);
        $('#button3,#button4').on('click', () => {
            if (flag === ONE & TWO) {
                // something...
            }
        })
    }
});

苦しみしかない地獄コードですね。分かりづらいし書きづらいです。

n回発火したら発動するイベント

たとえば「100回ボタンがクリックされたらくす玉が割れる」みたいなサービスを考えます(そんなのあるのか?)。これもふつうはカウンタ変数が必要ですが、async/awaitをつかえば分かりやすく書くことができます。

jQuery($ => {
    const click = el => new Promise(resolve => {
        $(el).one('click', resolve); // 要素がクリックされたらresolveする
    });
    (async () => {
        for (let i = 0; i < 100; i++) {
            await click('#button1');
        }
        console.log('You clicked #button1 100 times!!')
    })();
});

補足: RxJSについて

ちなみに今回やったこと、実はRxJSがやっていることに近いです。気になった人はそちらも調べてみてください