1. ryoyakawai

    No comment

    ryoyakawai
Changes in body
Source | HTML | Preview

この記事は WebAudio/WebMIDI API Advent Calendar 2017 Advent Calendar 2017 の2日目です。

TL;DR

Web MIDI APIを使うときに必ず使うAPIがrequestMIDIAccess()をasync/awaitを使って同期処理的な書き方で書き直して比べてみよう、というのがこのエントリーです。

PromiseとrequestMIDIAccess()

「async/awaitの話なのに、なんでPromiseなのか!」といきなり突っ込まれるかもしれませんが、追って説明しますので少々お待ちを。。。😅
さて、Web MIDI APIを使うときに最初に使うのがこのAPIです。実はこのAPIがWeb APIの中でPromiseが導入された初めてのAPIです(2013年にフラグ付きでShip)。そして現在ではPromiseはgetUserMedia()、Web Bluetooth, Web USBなどのデバイスへのアクセスを必要とするAPIのほとんどで使われるようになっています。

Promiseは便利なのだけど・・・

Promiseはとても便利なのですが「直感的な書き方ができないのがな〜・・」と感じたことはありませんか?そして「非同期処理だから仕方ない」と諦めていませんか?
そこで登場するのがasync/awaitです。async/awaitと使うと非同期処理なのに同期処理のような直感的な書き方をすることが可能です。
Promiseを使わない場合ですとネストが深くなり、Promiseの場合だと.then()がたくさん続くことになります。

Promiseとasync/awaitの関係

Promiseとは

例えば、引数として数字を渡しその数字が偶数の場合正常終了で「even」と出力、奇数の場合異常終了で「odd」と2秒後に出力するプログラムを考えたとします。
関数evenodd()が何をしているかを説明すると、
「引数を2で割った場合の判定としてゼロならばresolve()を実行し、0以外ならばreject()を実行」
です。
そしてresolve()reject()がこの場合は.then()で続けて書いた、
resolve()() => console.log('even')
reject()() => console.error('odd')
に対応しているのです。
まとめると、Promiseは全体の処理を「成功した場合」と「失敗した場合」の2つに分解して、それぞれの場合、次に何を実行するのかを定義する書き方がPromiseです。それぞれの場合で何を実行するかを「約束する」と表現すれば覚えやすいかな、と思います。

evenodd(2).then(() => console.log('even'), () => console.error('odd'));
function evenodd(num) {
  return new Promise( (resolve, reject) => {
    let result = num % 2; 
    setTimeout(() => {
      if(result == 0) {
        resolve();
      } else {
        reject();
      }
    }, 2000);
  });
}

そしてPromiseは非同期処理なので、例えevenodd()の一連の処理が終わらなくとも次の処理に進みます。また、続けて処理を.then()で続けることも可能です。

async/awaitとは

Promiseは非同期処理終了後の処理として.then()で定義することが可能ですが、長くなると辛いです。そこが同期的に書けるようにしているのがasync/awaitです。ですが、Promiseが前提として成り立っているので、Promiseの理解なしには、async/awaitも理解仕切れないです。つまりPromiseとasync/awaitは切っても切り離せない関係になっています。
それでは、上の偶数と奇数の例をasync/awaitに書き換えてみます。try {...} catch {...}と書き換えられ、resolve()が実行される場合は正常終了、reject()が実行される場合はcatchされるようになりました。Promiseが前提になっていますよね。というか進化系と言ってもいいのかもしれないです。

(async function(){
    try {
        await evenodd(2);
        console.log('even');
    } catch(error) {
        console.error('odd');
    }
    async function evenodd(num) {
        return new Promise( (resolve, reject) => {
            let result = num % 2;
            setTimeout(() => {
                if(result == 0) {
                    resolve(true);
                } else {
                    reject(false);
                }
            }, 2000);
        });
    }
}());

というのがPromiseとasync/awaitの関係です。
同期的な書き方になってとても見やすく感じると思います。いかがでしょう?

requestMIDIAccessを書き換える

Promiseを使って

馴染みのある書き方ですね。

<html>
  <head>
  </head>
  <body>

    <div>MIDI Input: <select id="midiinput"></select></div>
    <div>MIDI Output: <select id="midioutput"></select></div>

    <script>
     const MIDI = { inputs: [], outputs: [] };
     startMIDIAccess();

     function startMIDIAccess() {
         navigator.requestMIDIAccess({sysex:false}).then(successCallback, errorCallback)
         .then(() => {
                     //(!!!!....ここに続けて処理を書く....!!!!)
                });
         function successCallback(midiaccess) {
             const pullDevices = dIterator => {
                 let devices = []
                 for (let o = dIterator.next(); !o.done; o = dIterator.next()) {
                     devices.push(o.value)
                 }
                 return devices;
             };
             const midiDevices = {
                 inputs: pullDevices(midiaccess.inputs.values()),
                 outputs: pullDevices(midiaccess.outputs.values())
             }
             return midiDevices;
         };
         function errorCallback() {
             console.error('[ERROR] requestMIDIAccess()', error);
         }
     }
     function setDeviceToList(elemId, devices) {
         let elem = document.querySelector('#' + elemId)
         for(let i=0; i<devices.length; i++) {
             let option = new Option(devices[i].name, devices[i].id);
             elem.appendChild(option);
         }
     }
    </script>
  </body>
</html>

async/awaitを使って

上記と同じ内容をasync/awaitで書き換えました。同期的な処理になって分かりやすくなったと思いませんか?

<html>
  <head>
  </head>
  <body>

    <div>MIDI Input: <select id="midiinput"></select></div>
    <div>MIDI Output: <select id="midioutput"></select></div>

    <script>
     'use strict';
     (async function(){
         const MIDI = { inputs: [], outputs: [] };
         if(await startMIDIAccess() === true) {
             setDeviceToList('midiinput', MIDI.inputs);
             setDeviceToList('midioutput', MIDI.outputs);

             //(!!!!....ここに続けて処理を書く....!!!!)

         } else {
             console.error('[ERROR] Not able to get list of MIDI devices.')
         }

         async function startMIDIAccess() {
             try {
                 let access = await navigator.requestMIDIAccess({sysex:false});
                 let iItr = access.inputs.values(), oItr = access.outputs.values();
                 for(let i=iItr.next(); !i.done; i=iItr.next()) MIDI.inputs.push(i.value);
                 for(let o=oItr.next(); !o.done; o=iItr.next()) MIDI.outputs.push(o.value);
                 return true;
             } catch(error) {
                 console.error('[ERROR] requestMIDIAccess()', error);
                 return false;
             }
         }
         function setDeviceToList(elemId, devices) {
             let elem = document.querySelector('#' + elemId)
             for(let i=0; i<devices.length; i++) {
                 let option = new Option(devices[i].name, devices[i].id);
                 elem.appendChild(option);
             }
         }
     }());
  </script>
</html>

結局、何がいいの?

まずは見やすくなったと思います。
更に上記のMIDIデバイスをリストしてさらにその先の処理を続けて書く場合を考えてみましょう。
Promise版、async/await版のどちらにも(!!!!....ここに続けて処理を書く....!!!!)を挟んでいますが、ここに続けて処理を追加していくことになるのです。Promise版、async/await版のどちらがよさそうですか?
個人的にはasync/await版のが見やすいし、メンテナンスをすることを考えると直感的で分かりやすいくてasync/awaitのがうれしいです。

まとめ

async/await推し推しで書いてしまいましたが、実際に使ってみると「Promiseには戻れない・・・」と私は感じています。ほんとに見通しもよく、キレイでそして便利なんですよね。ということで、気になる方は是非試してみてくださいっ🎄

更新

  • 2018/6/27 シンプルなコードを更新しました