JavaScript
Node.js
promise
babel

ES7 async/awaitと、coがPromiseベースになっていたこと

More than 1 year has passed since last update.

Babelでasync/await使ってrequire('babel/polyfill')前提にするのは時期尚早すぎやしないかと思って、tj/co見たら4.0.0からPromiseベースになっていた。

まずPromiseとは

昔からJavaScriptの非同期処理といえば、setTimeoutのように引数としてコールバック関数を受けていた。しかし、非同期処理をチェインした場合ネストが深くなってコードの可視性が落ちる、所謂「コールバック地獄」という罠があった。

setTimeout(function() {
  console.log('A done');
  setTimeout(function() {
    console.log('B done');
    setTimeout(function() {
      console.log('C done');
      // コールバックした分無限にネストが深くなる
    }, 1000);
  }, 1000);
}, 1000);

こういうの。そして何より非同期処理のAPIがまちまちで、言語仕様の緩さと合わさってカオスな状況が生まれやすかった。これに対して、欠点を補いなおかつAPIを統一して扱いやすくするための仕様がPromiseである。

PromiseはNode v0.11.0以上とモダンブラウザの一部に実装されているので、Node v0.10系以前やレガシーブラウザ、IEで使いたい場合polyfillが必要になる。他に注意点として、NodeではPromiseが最新機能としてデフォルトでは利用できないので、--harmonyオプションをつけて起動しなくてはならない。ioはデフォルトで動く。

function doA() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('A done');
      resolve();
    }, 1000);
  });
}

function doB() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('B done');
      resolve();
    }, 1000);
  });
}

function doC() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('C done');
      resolve();
    }, 1000);
  });
}

doA()
  .then(doB)
  .then(doC);

このぐらいの内容だと逆に記述量は増えてしまうが、「then()catch()でチェインできる」と仕様を明示できるメリットは利便性や保守性に大きく寄与する。

Promiseのデメリット、coとは

PromiseによってAPIは統一されたが、非同期処理をチェインするには冗長な記述を繰り返す必要があり、同期処理ほど直感的に扱えなくなってしまうというデメリットがあった。

coはこのデメリットを解消し、JavaScriptの非同期処理を同期的に書けるようになるNPMパッケージである。他にもthunkやGeneratorも扱えるが、今回は混乱を防ぐためPromiseだけに単純化して書いている。

coの利便性を享受するには、Generator構文を直接扱えるNode v0.11.0以上か、Generator構文のためのpolyfillが必要になる。Generator構文もPromiseと同じようにNodeでは最新機能として扱われているので、--harmonyオプションをつけて実行しないと動かない。ioはGeneratorもデフォルトで動く。

$ npm install co
var co = require('co');

co(function* () {
  yield doA();
  yield doB();
  yield doC();
});

coに渡したGenerator構文の中ではyieldでPromiseの完了を待ち合わせてくれる。resolve()reject()に渡した引数もyieldの結果として取得できるので、同期処理の変数と同じ感覚で扱える。エラーハンドリングもPromiseのcatch()ではなく、try/catchが使える。

coがPromiseベースになったこと、そしてasync/await

coがPromiseベースになったことは、つまりco()した結果をthen()catch()でチェインできるということである。このことでcoがPromise実装の範疇として驚きのないものとして扱えるようになり、coとPromiseの相互運用が実現する。

Generator構文とPromiseはECMAScript 6として提案されており、coがGeneratorとPromiseによって成り立っているということは、coがECMAScript 6の要素によって成立している言い換えることができる。

さて、ここで改めてco()するコードを見てみると、構造がECMAScript 7として提案されているasync/awaitと酷似していることがわかる。

require('babel/polyfill');

(async function() {
  await doA();
  await doB();
  await doC();
})();
$ babel --stage 1 foo.js > bar.js

async/awaitはNodeやioでも実装されていないので、Babelで変換して実行するしかない。Babelにおいてもasync/awaitは提案レベルとしてデフォルトでは無効化されているので、--stageオプションにて1以下を指定しないと変換されないし、require('babel/polyfill')しないと実行できない。

coは4.0.0リリースの段階でECMAScript 7 async/awaitへの飛び石として再定義されており、このことからもECMAScript 7の一部がECMAScript 6からのステップアップとして実現されていることがよくわかる。もしasync/awaitが実装されれば、coからそのまま乗り換えることも可能になるだろう。

おまけ:BabelでGenerator構文が変換されてしまう

$ babel --blacklist regenerator baz.js

--blacklistオプションでregenerator transformerを無効化する。

参考