読者です 読者をやめる 読者になる 読者になる

MANA-DOT

PIXEL ART, PROGRAMING, ETC.

babelのasyncで遊んでみたメモ

babelのasyncで遊んでみたメモ

ES7から利用可能な async/await は非同期プログラミングの際に非常に魅力的な構文です。 babelを用いることによりES5の環境でもコードを実行可能です。

babelで非同期処理がどのように変換されるのか興味があったので、いろいろ遊んでみました。

async function

簡単なPromiseを用いた非同期コードとして以下の様な例があります。

function wait(msec) {
  return new Promise((resolve) => setTimeout(resolve, msec));
}

function main() {
  console.log('hoge');
  wait(2000).then(() => {
    console.log('fuga');
  });
}
main();

setTimeoutを用いた非同期なwait関数を呼ぶだけの例です。 async/awaitを用いると上のコードを次のように記述できます。

function wait(msec) {
  return new Promise((resolve) => setTimeout(resolve, msec));
}

async function main() {
  console.log('hoge');
  await wait(2000);
  console.log('fuga');
}
main();

ネストがなくなり、より人間に直感的な形で記述できるようになりました。

babelで変換してみる

babelを用いることで、上記async/awaitを利用したコードをES5の処理系で実行可能な形に変換できます。 babelは通常 regenerator-babel を用いて async/await を変換するようです。

function wait(msec) {
  return new Promise((resolve) => setTimeout(resolve, msec));
}

async function main() {
  console.log('hoge');
  await wait(2000);
  console.log('fuga');
}
main();

このコードは次のようになります。

function wait(msec) {
  return new Promise(function (resolve) {
    return setTimeout(resolve, msec);
  });
}

function main() {
  return regeneratorRuntime.async(function main$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
      case 0:
        console.log('hoge');
        context$1$0.next = 3;
        return regeneratorRuntime.awrap(wait(2000));

      case 3:
        console.log('fuga');

      case 4:
      case 'end':
        return context$1$0.stop();
    }
  }, null, this);
}
main();

regeneratorを用いたやや複雑なコードが吐き出されました。 awaitのタイミングで main$ の実行が中断され、 wait(2000) が終了すると再度呼び出されるが、 context$1$0.next の値が変化してるため中断された箇所から実行されるのであろうことはなんとなく想像できます。

制御構文

上記のような単純なコードの変換は納得ができますが、if、whileなどの制御構文がどうなるかも気になります。

async function getWithRetry(retryCount) {
  let result = false;
  for (let i = 0; i < retryCount; i++) {
    try {
      result = await getSomething();
    } catch(e) {}
    if (result !== false) {
      break;
    }
    console.log('retry');
  }
  if (result) {
    return result;
  }
  throw new Error('fail!');
}

例えば上記のような、取得できるまでリトライするコードを変換してみます。

function getWithRetry(retryCount) {
  var result, i;
  return regeneratorRuntime.async(function getWithRetry$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
      case 0:
        result = false;
        i = 0;

      case 2:
        if (!(i < retryCount)) {
          context$1$0.next = 17;
          break;
        }

        context$1$0.prev = 3;
        context$1$0.next = 6;
        return regeneratorRuntime.awrap(getSomething());

      case 6:
        result = context$1$0.sent;
        context$1$0.next = 11;
        break;

      case 9:
        context$1$0.prev = 9;
        context$1$0.t0 = context$1$0['catch'](3);

      case 11:
        if (!(result !== false)) {
          context$1$0.next = 13;
          break;
        }

        return context$1$0.abrupt('break', 17);

      case 13:
        console.log('retry');

      case 14:
        i++;
        context$1$0.next = 2;
        break;

      case 17:
        if (!result) {
          context$1$0.next = 19;
          break;
        }

        return context$1$0.abrupt('return', result);

      case 19:
        throw new Error('fail!');

      case 20:
      case 'end':
        return context$1$0.stop();
    }
  }, null, this, [[3, 9]]);
}

このコードを見ると、case説のそれぞれがラベルになってて context$1$0.next を指定して break するのが GOTO、 context$1$0.next に次の行を指定してPromise返すのがawaitなんだなとなんとなく理解できますね。 (breakで switch を抜けると直上に while(1) があるため、再度実行されて context$1$0.next で指定した箇所から再開される)

気になるのが元のコードでtry-catchしてる箇所ですが、 regeneratorRuntime.async の第四引数が tryLocsList であり、そこに渡っている [3,9] が、 3番で例外が発生した場合のキャッチ節が9番であることを知らせているようで、 getSomething で例外が発生した場合は9番に入る模様ですね。

case節を使ってラベル+GOTOを表現するのが面白いですね。

asyncToGenerator

ところで、asyncToGenerator オプションを使用することで、regeneratorを使わずasync/awaitをES6 generatorを用いたコードを出力できるようです。

function wait(msec) {
  return new Promise((resolve) => setTimeout(resolve, msec));
}

async function main() {
  console.log('hoge');
  await wait(2000);
  console.log('fuga');
}
main();

このコードは次のように変換されます。

var main = _asyncToGenerator(function* () {
  console.log('hoge');
  yield wait(2000);
  console.log('fuga');
});

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; }

function wait(msec) {
  return new Promise(function (resolve) {
    return setTimeout(resolve, msec);
  });
}

main();

_asyncToGenerator がやや複雑であるものの、main関数はregeneratorを用いた変換と比べ、かなり素直に変換されています。 この形は、Async Functions の Informative Desugaringの項とほぼ同じですね。 _asyncToGenerator 関数は spawn 関数と形がやや違うもののやってることはほぼ同じです。 asyncToGeneratorオプションは上記desugaringの実装と言えそうです。

制御構文

async function getWithRetry(retryCount) {
  let result = false;
  for (let i = 0; i < retryCount; i++) {
    try {
      result = await getSomething();
    } catch(e) {}
    if (result !== false) {
      break;
    }
    console.log('retry');
  }
  if (result) {
    return result;
  }
  throw new Error('fail!');
}

先ほどの制御構文を用いたコードも変換してみます。

var getWithRetry = _asyncToGenerator(function* (retryCount) {
  var result = false;
  for (var i = 0; i < retryCount; i++) {
    try {
      result = yield getSomething();
    } catch (e) {}
    if (result !== false) {
      break;
    }
    console.log('retry');
  }
  if (result) {
    return result;
  }
  throw new Error('fail!');
});

こちらでも変わらず、素直に変換されました。

asyncToGenerator → regenerator

ちなみに、asyncToGeneratorで変換したコードをregeneratorで再変換すると次のようになります。

var main = _asyncToGenerator(regeneratorRuntime.mark(function callee$0$0() {
  return regeneratorRuntime.wrap(function callee$0$0$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
      case 0:
        console.log('hoge');
        context$1$0.next = 3;
        return wait(2000);

      case 3:
        console.log('fuga');

      case 4:
      case 'end':
        return context$1$0.stop();
    }
  }, callee$0$0, this);
}));

function _asyncToGenerator(fn) { /** 省略 */ }

function wait(msec) {
  return new Promise(function (resolve) {
    return setTimeout(resolve, msec);
  });
}

main();

main関数の形が最初のregeneratorを用いた変換に近い形になりました。 regeneratorRuntime.async実装 を見てみると、 _asyncToGenerator 相当の処理を regeneratorRuntime.wrap した関数に対して行っていることがわかり、変換→再変換したコードとだいたい一緒であることがわかります。面白いですね。

感想

regeneratorによる変換はbabelのほかのES6の変換と違い、もとの文脈をかなり破壊するのでちょっと怖いし、パフォーマンスも心配です。デバッグもやりづらそう。 遊びコードなら使うと楽しそうですけど、真面目コードで使うのは少し不安ですね。

対してasyncToGeneratorによる変換はかなり素直な変換なので、 generatorを利用できる環境ならば、asyncToGeneratorを使うと結構安心して使えそうです。

参考リンク