More than 1 year has passed since last update.

この記事は、JavaScript stage 0,1,2,3 Advent Calendar 2015の7日目です。

この記事では、Async Functionsを紹介します。

記事内のJavaScriptコードは、ES2015を前提としています。

また、Promiseに関する基本的な知識を前提とします。
Promiseに明るくない方はJavaScript Promiseの本を読むといいでしょう。素晴らしいテキストです。

Async Functionsとは

Async Functionsは、非同期処理の記述をより同期的に行うための新文法です。
現在はEdgeにのみ実装されています(「試験的なJavaScript機能を有効にする」必要あり)。
2015年11月末を目処にStage 4(Finished)となる見込みでしたが、12月7日現在も依然としてStage 3(Candidate)のままになっています。

どのように書けるか

Promiseの登場、そしてAsync Functionの登場によって非同期処理がどのように書けるようになったか、簡単な例を示します。Promise以前、以後、Async Function以後の3つのコードは、すべて同じ結果を得ることができます。

Promise以前

function someAsyncRoutine1(n, callback) {
  setTimeout(function(){
    console.log(n);
    callback(n+1);
  }, 1000);
}

someAsyncRoutine1(1, function(result1){
  someAsyncRoutine1(result1, function(result2){
    someAsyncRoutine1(result2, function(result3){
      console.log('done!!');
    });
  });
});

終了を通知するコールバックの中でさらにコールバックを書いています。前の非同期処理に依存する内容が続く場合、このようにネストはどんどん深くなっていきます。いわゆるコールバック地獄と呼ばれているものです。

Promise以後

function someAsyncRoutine2(n) {
  return new Promise(resolve => {
    setTimeout(()=>{
      console.log(n);
      resolve(n+1);
    }, 1000)
  });
}

someAsyncRoutine2(1)
  .then(n => // someAsyncRoutine2相当の内容
    new Promise(resolve => {
      setTimeout(()=>{
        console.log(n);
        resolve(n+1);
      }, 1000)
    })
  ).then(someAsyncRoutine2)
  .then(()=>console.log('done!!'));

非同期処理を行う関数は、その処理の終了時にresolveされるpromiseオブジェクトを返すようにします。ネストを必要以上に深くすることなく、非同期処理の流れを記述できるようになりました。また、promiseオブジェクトを参照すれば非同期処理が成功したかどうか、終了しているかどうかを得ることができるようになりました。

Async Function以後

// someAsyncRoutine2 を使います

async function someAsyncRoutine3(init) {
  let n;
  n = await someAsyncRoutine2(init);
  n = await someAsyncRoutine2(n);
  n = await someAsyncRoutine2(n);
  console.log('done!!');
}

someAsyncRoutine3(1);

asyncawaitといった新たなキーワードが登場しましたが、これらに目をつぶれば、一般的な同期処理と同様に書くことができているとわかります。
もちろん同期処理と同様に書くことができるだけで、ちゃんと非同期で処理されます。

新たなキーワード async/await

async

asyncキーワードは、たとえばasync function hoge(){}とすることで、関数hogeがAsync Functionであることを指定します。無名関数や即時関数、ES2015で追加されたArrow Function、class構文中のメソッドにも問題なく付加することができます。

async function hoge(){}

let fuga = async () => {}

class Piyo {
  constructor(){}
  async piyopiyo() {}
}

await

awaitキーワードは、Async Functionの中でしか使用することができません。

awaitキーワードは前置単項演算子のように振る舞い、受け取った単項式とあわせてAwait式(Await Expression)とされます。与えられる単項式がpromiseオブジェクトを返す場合、そのPromiseがresolveされるまで関数の処理が止まります。promiseオブジェクトがresolveされる時、引数として渡された値がアンラップされて、Await式の評価結果となります。

function add(a, b) {
  return new Promise(resolve => resolve(a + b));
}
(async function(){
  let n = 1 + (await add(2, 3)) + 4;
  console.log(n); // 10
})();

awaitキーワードが受け取る単項式は、promiseオブジェクトが返らなくても構いません。どのような値が返ろうとも、即時解決されるpromiseオブジェクトにラップされた後、同様にアンラップされる例が示されています。するとpromiseオブジェクトが返る場合には、promiseオブジェクトで多重にラップされることになりますが、アンラップが発生する場合はその全てが一度にアンラップされるため、同じ結果になります。

エラーハンドリング

Async FunctionではPromiseからアンラップして値を得ますが、エラーも同様に一般的なtry-catch文によって管理することができます。

ショートハンドとして、その場でcatchしてしまうことも考えられます。

catchされなかった場合には、Async Functionの返値であるpromiseオブジェクトが失敗するため、Promiseの.catch()で拾うことができます。

function shouldBeFail(){
  return Promise.reject(new Error('fail'));
}

(async function(){
  try{
    await shouldBeFail();
  }catch(e){
    console.error(e); // fail
  }

  let t = await shouldBeFail().catch('caught'); // 'caught'

  await shouldBeFail();
})().catch(e => console.error(e)); // 'fail'

非同期なループの実現

Async Functionの出現によってもっとも劇的に改善されるのは、非同期処理のループ記述でしょう。
コールバック時代、またPromiseの出現によっても、何かの条件を確認しながら非同期処理を繰り返すような処理の記述は、非常に面倒でした。今でもおそらくは、async.jsやその派生種を使わざるを得ない場面であるはずです。

function someAsyncFn(n) {
  return new Promise(resolve => {
    setTimeout(()=>{
      console.log(n);
      resolve(n+1);
    }, 1000)
  });
}

function someAsyncCond(n) {
  return new Promise(resolve => {
    resolve(n < 7);
  });
}

(async function(){
  for(let i = 0; await someAsyncCond(i); ++i){
    await someAsyncFn(n);
  }
})();

このように非同期処理のループを簡単に書くことができます。フロントエンドのE2Eテストなどには、MutationObserver等と合わせて抜群に効果を発揮することでしょう。

今すぐ試す

ES5以前の環境

babelやtraceur等のトランスパイラを使用することにより、Async Functionを試すことができます。

ES2015(ES6)環境

あなたがNode v4以降を使用しているなら、デフォルトでGeneratorが使えるはずです。

勘の良い方はお気付きでしょうが、実を言えばこのAsync Functionは糖衣構文に過ぎません。PromiseとGeneratorがある環境でなら同様のことが既に実現できます。

次のような関数を定義しておきます。引用元:Async Functions

function spawn(genF, self) {
  return new Promise(function(resolve, reject) {
    var gen = genF.call(self);
    function step(nextF) {
      var next;
      try {
        next = nextF();
      } catch(e) {
        // finished with failure, reject the promise
        reject(e);
        return;
      }
      if(next.done) {
        // finished with success, resolve the promise
        resolve(next.value);
        return;
      }
      // not finished, chain off the yielded promise and `step` again
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

上述の関数を前提とすると、Async Functionの構文は次のように変換ができます。

// Async Function
async function hoge(){
  await fooAsync();
}

// Promise + Generator + polyfill
function hoge(){
  return spawn(function*(){
    yield fooAsync();
  });
}

あなたも今すぐAsync Functionを試して、非同期処理を軽快に書いてみませんか。