gulpのタスクを同期的に実行する方法


gulpのタスクは基本的に非同期で実行されます。例えば以下のコードでは、ファイルコピーが完了する前に "done" と表示される可能性があります。

```javascript
gulp.task('copy', function() {
  // ファイルをコピー
  gulp.src('src/file').pipe(gulp.dest('dest'));
});

// copyに依存するタスク
gulp.task('done', ['copy'], function() {
  // ファイルコピー完了「前」に実行される！！
  console.log("copy done");
});
```

gulpが高速に動作する理由の一つはこの非同期性ですが、どうしても同期的に処理したい場合（すなわち特定のタスクの完了を待ってから別なタスクを実行したい場合）もあると思います。

この記事では同期的にタスクを実行する方法として、[gulp API docs](https://github.com/gulpjs/gulp/blob/master/docs/API.md#async-task-support)に記載されているもの、そしてその発展形を紹介します。

## タスク内の非同期処理が一つだけの場合
### Streamをreturnする
まずは1番お手軽な方法から。タスクの最後にStreamをreturnするだけです。タスク内の非同期処理が一組だけの場合はこれで十分でしょう。

```javascript
gulp.task('copy', function() {
  // ファイルをコピーしてそのStreamをreturnする
  return gulp.src('src/file').pipe(gulp.dest('dest'));
});

// copyに依存するタスク
gulp.task('done', ['copy'], function() {
  // ファイルコピー完了後に実行される
  console.log("copy done");
});
```

### タスク完了callbackを利用する
```gulp.task(name, function(callback) {});``` のようにタスクがcallback引数を受け取る場合、callbackが実行されるまでgulpは次のタスクの実行を控えます。この機能と「Streamの'end'イベント」を組み合わせても、同期的なタスク実行を実現できます。

（Streamの'end'イベントについては[node.jsのドキュメント](http://nodejs.org/api/stream.html#stream_stream)に詳しく書かれています。gulpのStreamもnode.jsのStreamを継承しています。）

```javascript
gulp.task('copy', function(callback) {
  // ファイルをコピーし'end'イベントをハンドル
  // 'end'イベントはファイルコピー完了時に実行される
  gulp.src('src/file').pipe(gulp.dest('dest')).on('end', function() {
    // callbackを実行してgulpにタスク完了を通知
    callback();
  });
});

// copyに依存するタスク
gulp.task('done', ['copy'], function() {
  // ファイルコピー完了後に実行される
  console.log("copy done");
});
```

## タスク内の非同期処理が複数の場合

タスク内に非同期処理が複数ある場合は少し工夫が必要です。基本は前述の「タスク完了callbackを利用する方法」と同じですが、'end'イベントハンドラの中で直接callbackを実行するのではなく、 **待つべき非同期処理の回数分だけ呼び出された後に、callbackを実行する** そんな処理を実行します。

言葉で説明するとややこしいですが実装は簡単です。

```javascript
gulp.task('copy', function(callback) {

  var wait_max   = 3; // 完了を待つべき非同期処理の数
  var wait_count = 0; // すでに完了した非同期処理の数

  // 'end'イベントハンドラから呼び出される処理
  function onEnd() {
    if (wait_max === ++wait_count) {
      // callbackを実行してgulpにタスク完了を通知
      callback();
    }
  }

  // ファイルをコピー
  gulp.src('src/file1').pipe(gulp.dest('dest1')).on('end', function() {
    onEnd();
  });

  gulp.src('src/file2').pipe(gulp.dest('dest2')).on('end', function() {
    onEnd();
  });

  gulp.src('src/file3').pipe(gulp.dest('dest3')).on('end', function() {
    onEnd();
  });
});

// copyに依存するタスク
gulp.task('done', ['copy'], function() {
  // ファイルコピー完了後に実行される
  console.log("copy done");
});
```

また、このような処理が何箇所にも出てくる場合は、以下のように再利用できるコードを書いておくと良いでしょう（もちろん他の実装方法もあると思います。例えば[underscore](http://underscorejs.org/)の[after](http://underscorejs.org/#after)を利用しても同等の事が可能でしょう）。

```javascript
/*
  関数を遅延実行（deferred）するオブジェクト
    -- untilで指定したeventを全て待ち合わせてからexecで指定した関数を実行する。
*/
var Defer = function() {

  var wait_max = 0, wait_count = 0, callback = null;

  function onEventEnd() {
    if (max === ++count) {
      callback && callback();
    }
  }

  this.until = function(ev) {
    max++;
    ev.on('end', onEventEnd);
  };

  this.exec = function(cb) {
    callback = cb;
  };
};
```

このDeferを使えば前述の例を以下のように書けます。

```javascript
gulp.task('copy', function(callback) {

  var d = new Defer();

  // ファイルをコピー
  d.until(
    gulp.src('src/file1').pipe(gulp.dest('dest1')));
  d.until(
    gulp.src('src/file2').pipe(gulp.dest('dest2')));
  d.until(
    gulp.src('src/file3').pipe(gulp.dest('dest3')));

  d.exec(function() {
    // callbackを実行してgulpにタスク完了を通知
    callback();
  });
});

// copyに依存するタスク
gulp.task('done', ['copy'], function() {
  // ファイルコピー完了後に実行される
  console.log("copy done");
});
```