ES2015のGenerator、および Async/Await を使った非同期処理について

マネーフォワードでフロントエンドの開発者をしています大塔と申します。

今回は個人的にお世話になることの多いGeneratorおよび Async/Await を使って非同期処理を、同期処理っぽく書く方法について、記載させていただきたいと思います。

環境構築

webpackrollupなど新しいバンドラーが出てきていますが、今回はECMAScriptのビルドにgulpを使います。またトランスパイラにはBabelを用います。

  • NodeJS 4.2.1
  • npm 2.14.7
  • Google Chrome 49.0.2623.112 (64-bit)

ソースコード

今回作成したソースコードはすべてこちらからダウンロードできます。ソースコードのビルド方法や動かし方はお手数ですがリンク先をご覧ください。

package.json

必要となるパッケージを記述します。

{
  "name": "blog_sample",
  "version": "1.0.0",
  "description": "Just a trivial blog sample",
  "main": "gulpfile.js",
  "author": "AtaruOhto",
  "license": "MIT",
  "devDependencies": {
    "babel-plugin-transform-runtime": "^6.7.5",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-stage-3": "^6.5.0",
    "babelify": "^7.2.0",
    "browserify": "^12.0.1",
    "glob": "^7.0.3",
    "gulp": "^3.9.0",
    "gulp-load-plugins": "^0.10.0",
    "gulp-plumber": "^1.0.1",
    "gulp-util": "^3.0.6",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "watchify": "^3.6.1"
  },
  "dependencies": {
    "babel-runtime": "^6.6.1",
    "bluebird": "^3.3.5",
    "express": "^4.13.4",
    "sleep": "^3.0.1",
    "superagent": "^1.8.3"
  }
}

gulpfile.js

ECMAScriptをgulpでバンドルする処理を記述しています。

'use strict';

const gulp = require('gulp');
const plugins = require('gulp-load-plugins')();
const babelify = require('babelify');
const browserify = require('browserify');
const watchify = require('watchify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const glob = require('glob');

let startWatchify = () => {

    const srcDir = 'src/'; // コンパイル対象ファイルのディレクトリ名
    const distDir = './dist'; // コンパイル先ディレクトリ
    const sources = glob.sync(`${srcDir}**/*.js`); // コンパイル対象のファイル

    sources.forEach((entryPoint) => {
        let distFileName = entryPoint.replace(srcDir, '');

        // browserify: オプション群
        let browserifyOptions = {
            // コンパイル対象となるファイル
            entries: [entryPoint],
            // babel-transform-runtime, e2015, stage-2 プリセットを適用しつつ、babelifyを使って対象をコンパイルする。
            // http://babeljs.io/docs/plugins/
            transform: babelify.configure({presets: ['es2015', 'stage-3'], plugins: ['transform-runtime']}),
            debug: true,
            //watchifyの差分ビルドを有効化
            cache: {},
            packageCache: {}
        };

        let watchifyStream = watchify(browserify(browserifyOptions));

        let execBundle = () => {
            plugins.util.log(`${entryPoint}をビルドしています...`);
            return watchifyStream
                .bundle()// バンドル化
                .on('error', plugins.util.log.bind(plugins.util, 'Browserify Error'))//Errorが発生した場合にはログに出力
                .pipe(plugins.plumber())//Errorが発生してもタスクを止めない
                .pipe(source(distFileName))//streamingをvinyl file objectへと変換する
                .pipe(buffer())//vinyl file objectをvinyl buffered object形式に変換する
                .pipe(gulp.dest(distDir));//distディレクトリに出力
        };

        // 対象ファイルの変更を検知
        watchifyStream.on('update', execBundle);
        watchifyStream.on('log', plugins.util.log);
        return execBundle();
    });

};

gulp.task('default', startWatchify);

ジェネレーター (Generator)を使った非同期処理

一般的なJavaScriptの関数は呼び出された場合、通常その関数の最後の行まで実行されます。それに対して、ES2015の Generator は関数の途中で実行を止めることができます。そして、「次の処理に移ってください」という通知を受けとることによって、 Generator は処理を中断したところから、処理の実行を再開します。 Generatorfunction*() というように関数に*マークをつけることで定義できます。今回はこの Generator とサードパーティーの Promise モジュールを使って非同期処理を同期処理っぽくネスト無しで書く例を記載してみます。

bluebirdqcoなどのライブラリはジェネレーターと併用できる機能を備えており、 Generator とともに用いることで非同期処理を同期処理のように記述することができます。下記の例では bluebirdcoroutine()関数を用いています。下記の例では yield キーワードを記述している箇所でジェネレーターは処理を一時停止して、yield キーワードがつけられた関数内部で Promise が解決 (resolve) した時に、戻り値を受け取った上で処理を再開します。

'use strict';
import Promise from 'bluebird';

let serveAppetizers = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(' 前菜の準備ができました ');
            resolve(' 前菜 '); // 戻り値を返して、処理のコントロールをGeneratorに返す。
        }, 1000);
    });
};

let serveSoup = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(' スープの準備ができました ');
            resolve(' スープ ');
        }, 100);
    });
};

let serveMainDish = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(' メインディッシュの準備ができました ');
            resolve(' メインディッシュ ');
        }, 1000);
    });
};

let serveDesertAndCoffee = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(' デザートとコーヒーの準備ができました ');
            resolve(' デザートとコーヒー ');
        }, 100);
    });
};

let serveFullCourse = () => {
    return Promise.coroutine(function* () {
        let set = '';
        set += yield serveAppetizers();      //serveAppetizers()がresolveするまで待機
        set += yield serveSoup();            //serveSoup()がresolveするまで待機
        set += yield serveMainDish();        //serveMainDish()がresolveするまで待機
        set += yield serveDesertAndCoffee(); //serveDesertAndCoffee()がresolveするまで処理を待機
        console.log(`お客様に${set}を提供しました。`);
    })();
};

serveFullCourse();

上記直列処理の実行結果は下記のようになります。Generator内の処理が上から順番に実行されています。

Async/Await を使った非同期処理

Async/Await は非同期処理を同期処理のように書くことのできる次期ECMAScriptで策定中の機能です。asyncというキーワードがついた関数を定義して、その中でawaitというキーワードを関数につけて用いることで、非同期の関数処理が完了するまで処理を待機します。処理の委譲先である非同期の関数はPromiseオブジェクトを返し、そのPromiseが解決された時に処理のコントロールをasyncの関数に戻します。

Async/Await でサーバーからAjaxで画像のURLの配列を取得してきます。処理のメインストリームとなる関数には async というキーワードをつけます。処理を待機する非同期の関数にはawaitというキーワードをつけます。下記の例ではNodeJSで立てたモックの簡易サーバーにAjaxでリクエストを送り、画像のURLを取得してきます。

server.js

'use strict';

const express = require('express');
const app = express();
const sleep = require('sleep')

app.set('port', (3333));
app.use('/', express.static('./'));

app.get('/images', (req, res) => {
    sleep.sleep(1);
    res.send([
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images/bnr_moneyforward.png',
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images/bnr_mfcolud01.png',
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images/bnr_mfcolud02.png',
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images//bnr_mfcolud03.png',
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images/bnr_mfcolud04.png',
        'http://corp.moneyforward.com/wp-content/themes/wordpress/images/bnr_mfcolud05.png'
    ]);
});

app.listen(app.get('port'), function() {
    console.log('Please visit http://localhost:' + app.get('port') + '/');
});

async_await.js

'use strict';
import req from 'superagent';

let getImagesWithAjax = ()=> {
    return new Promise((resolve, reject) => {
        const url = 'http://localhost:3333/images';
        // サーバーから画像URLの配列を取得
        req
            .get(url)
            .end((err, res) => {
                if (err) return reject(err);
                resolve(res.body);
            });
    });
};

// asyncキーワードを関数に付与する
let showImages = async () => {
    try {
        let images = await getImagesWithAjax(); //getImageWithAjaxが終了するまで待機
        const manipulationRoot = document.querySelector('.js-manipulation-root');
        images.forEach((img) => {
            manipulationRoot.insertAdjacentHTML('beforeend', `<div style="display: inline-block; width: 200px;"><img src="${img}" /></div>`);
        });
    } catch (err) {
        console.error(`処理中にエラーが発生しました。 ${err} `);
    }
};

showImages();

下記が実行結果となります。Ajaxによる画像の取得まで待機した後で、取得してきた画像をimgタグで表示しています。

まとめ

  • GeneratorAsync/Await を使うことで非同期処理をネストやthen()なしで同期処理のように書くことができます。jQueryのdefferedのようにthen()で関数をつなげる必要もなく、すっきり記述できます。
  • 現時点 (2016年4月18日) ではブラウザ未搭載なので、Babel、Traceurなどのトランスパイラを使用する。babelで使うならばpolyfill等をインクルードしてくれるtransform-runtimeがビルド時に必要。

最後に

マネーフォワードではJS、CSS大好きなフロントエンドエンジニアを募集しています。
ご応募お待ちしています。

【採用サイト】
マネーフォワード採用サイト
Wantedly | マネーフォワード

【プロダクト一覧】
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad
家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Android
クラウド型会計ソフト『MFクラウド会計』
クラウド型請求書管理ソフト『MFクラウド請求書』
クラウド型給与計算ソフト『MFクラウド給与』
経費精算システム『MFクラウド経費』
消込ソフト・システム『MFクラウド消込』
マイナンバー対応『MFクラウドマイナンバー』
創業支援トータルサービス『MFクラウド創業支援サービス』
お金に関する正しい知識やお得な情報を発信するウェブメディア『マネトク!』

Pocket