Help us understand the problem. What is going on with this article?

ES2017 async/await + Promise で解決できる事、とES2015(ES6) generators (yield) + Promise + npm aa (async-await) で解決できる事

More than 1 year has passed since last update.

ES2017 async/await + Promise で解決できる事

ES2017 async/awaitPromise を使うと非同期処理をすごく簡単に処理できる。

とても便利なのだが、それだけでは、どうも機能が足りない様に見える。

この記事は... TL;DR
ES2017 async/await を使っても、まだいろいろと課題は残ってるよ。
ES2015 (ES6) generators と npm aa (async-await) だと、より良い解決策があるよ。って話。

以下の図の様な非同期処理フローを考えてみる。

es7.png

横軸は時間だ。左から右へ時間が流れていくものと考えて欲しい。
縦線は複数の処理を同期させたい、という意味だ。

ES2017 async/await 版のコード例を見てみよう。

es2017-async-await-ex.js
    'use strict';

    //require('babel-polyfill'); // おまじない
    //require('regenerator').runtime();

    console.log('main: start');
    main().then(function (val) {
        console.log('main: finish:', val);
    });

    async function main() {
        var result;
        console.log('main: started');

        // シーケンシャル処理(逐次処理)
        result = await sleep(200, 'a1');
        console.log('main-a1: sleep: a1 =', result);
        result = await sleep(100, 'a2');
        console.log('main-a2: sleep: a2 =', result);
        result = await sleep(300, 'a3');
        console.log('main-a3: sleep: a3 =', result);

        // パラレル処理(並行処理)
        result = await Promise.all([sleep(200, 'b1'), sleep(100, 'b2'), sleep(300, 'b3')]);
        console.log('main-b : Promise.all([promises]): [b1, b2, b3] =', stringify(result));

        // asyncなsub()をawaitで呼ぶ
        result = await sub('c');
        console.log('main-c : sub(c) =', result);
        // asyncなsub()を並行処理で呼ぶ
        result = await Promise.all([sub('d1'), sub('d2')]);
        console.log('main-d : Promise.all([promises]): [d1, d2] =', stringify(result));

        return 'return value!';
    }

    async function sub(val) {
        await sleep(100, val + '-x1');
        console.log('sub-' + val + '-x1: sleep: ' + val + '-x1');

        await sleep(100, val + '-x2');
        console.log('sub-' + val + '-x2: sleep: ' + val + '-x2');

        return val;
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

    function stringify(object) {
        var str = '';
        if (object instanceof Array) {
            for (var i = 0, n = object.length; i < n; ++i)
                str += (str ? ', ' : '') + object[i]
            return '[' + str + ']';
        }
        else {
            for (var key in object)
                str += (str ? ', ' : '') + key + ':' + object[key]
            return '{' + str + '}';
        }
    }

最初の require('babel-polyfill') は、本来のES2017では不要だと思うけどBabelを使う上での、おまじないだ。

await sleep(200, 'a1'); の部分は非同期処理を開始して、制御を他に譲って、
非同期処理が終わった時に戻ってくる構文だ。

  • シーケンシャルな処理であれば、await を使うだけで良い。
  • 更に深く処理を実行したければ await sub(); で良い。

やった。これでいいじゃん。と思うけど...

でも、パラレルな処理をやりたければ、Promise.all([promises,...]); などを使う必要がある。

  • 処理を2つにわけたかったら? 合流したかったら? → Promise.all を使う
  • 合流せずに途中結果だけを同期したかったら? → ???
  • 繰り返しイベントが発生する様なものは? ストリームは? インターバルタイマーは? → ???

ちょっと欲しい機能が足りないんじゃないかな。
更に、ES2017は今はまだブラウザでは標準とは言い難い。

ブラウザではまだ動かない。Babel が必要だね。それでいいのかな。

記事書いている時にbabel 6.0が出た。plugin を使うと良い。

ES2015 (ES6) generators (yield) + Promise + npm aa (async-await) で解決できる事

ES2015 (ES6) generators (yield) と Promise + npm aa (async-await) を使っても非同期処理をすごく簡単に処理できる。

以下の図の様な非同期処理フローを考えてみる。

es2015.png

ES2015 (ES6) generators (yield) 版のコード例を見てみよう。

es2015-async-await-ex.js
    // require('regenerator').runtime()           // babel & browserifyするなら入れておく
    // var aa = require('aa');
    var aa = (this && this.aa) || require('aa');  // 普通は var aa = require('aa'); で良い
    var Promise = aa.Promise;                     // native Promiseがあれば不要

    console.log('main: start');
    aa(main()).then(function (val) {
        console.log('main: finish:', val);
    });

    function *main() {
        var result;
        console.log('main: started');

        // シーケンシャル処理(逐次処理)
        result = yield sleep(200, 'a1');
        console.log('main-a1: sleep: a1 =', result);
        result = yield sleep(100, 'a2');
        console.log('main-a2: sleep: a2 =', result);
        result = yield sleep(300, 'a3');
        console.log('main-a3: sleep: a3 =', result);

        // パラレル処理(並行処理) ... 配列やオブジェクトで結果を取得
        result = yield [sleep(200, 'b1'), sleep(100, 'b2'), sleep(300, 'b3')];
        console.log('main-b : parallel Array :', stringify(result));
        result = yield {x:sleep(200, 'c1'), y:sleep(100, 'c2'), z:sleep(300, 'c3')};
        console.log('main-c : parallel Object:', stringify(result));
        result = yield Promise.all([sleep(200, 'd1'), sleep(100, 'd2'), sleep(300, 'd3')]);
        console.log('main-d : Promise.all([promises,...]): [d1, d2, d3] =', stringify(result));

        // generatorのsub()をyieldで呼ぶ
        result = yield sub('e');
        console.log('main-e : sub(e) =', result);

        // パラレル処理(並行処理) ... 配列やオブジェクトで結果を取得
        // generatorのsub()を並行処理で呼ぶ ... 配列
        result = yield [sub('f1'), sub('f2')];
        console.log('main-f : [generator,...]: [f1, f2] =', stringify(result));
        // generatorのsub()を並行処理で呼ぶ ... オブジェクト
        result = yield {x:sub('g1'), y:sub('g2')};
        console.log('main-g : {x:generator, y:...}: {x:g1, y:g2} =', stringify(result));

        // 必要ないけど無理やりgeneratorのsub()をpromiseにしてみた
        result = yield Promise.all([aa(sub('h1')), aa(sub('h2'))]);
        console.log('main-h : Promise.all([aa(generator),...]): [h1, h2] =', stringify(result));

        return 'return value!';
    }

    function *sub(val) {
        yield sleep(100, val + '-x1');
        console.log('sub-' + val + '-x1: sleep: ' + val + '-x1');

        yield sleep(100, val + '-x2');
        console.log('sub-' + val + '-x2: sleep: ' + val + '-x2');

        return val;
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

    function stringify(object) {
        var str = '';
        if (object instanceof Array) {
            for (var i = 0, n = object.length; i < n; ++i)
                str += (str ? ', ' : '') + object[i]
            return '[' + str + ']';
        }
        else {
            for (var key in object)
                str += (str ? ', ' : '') + key + ':' + object[key]
            return '{' + str + '}';
        }
    }

ES2017版の async function ()function *() にして、awaityield にすれば、ほぼ同じコードだ。

Node.js版は...
npm aa (async-await) を使うだけで、
ES2015 (ES6) generators (yield) はもう標準なので、そのまま動く。
※Babel を使わなくても良い

$ npm install --save aa
$ node es2015-async-await-ex

HTML版の Chrome, Firefox, Edge では...
npm aa (async-await) と依存している npm promise-thunk を使うだけで、
ES2015 (ES6) generators (yield) はもう標準なので、そのまま動く。
※Babel, Browserify を使わなくても良い

es2015-async-await-ex.html
<!doctype html>
<meta charset="UTF-8">
<script src="https://lightspeedworks.github.io/console-to-window/console-to-window.js"></script>
<script src="https://lightspeedworks.github.io/promise-thunk/promise-thunk.js"></script>
<script src="https://lightspeedworks.github.io/aa/aa.js"></script>
<script src="es2015-async-await-ex.js"></script>

HTML版の Internet Explorer では...
Babel, Browserify を使ってください。

合流せずに途中結果だけを同期したかったら?

以下の図の様な非同期処理フローを考えてみる。

es2015-fork-join.png

npm aaaa.Channel を使うと自由に同期できる。yield chan;とすると同期できる。
処理を分割(fork)して合流(join)したい時、generatorの配列を使うと良い。

es2015-fork-join.js
    var aa = require('aa');
    var Channel = aa.Channel;

    console.log('main: start');
    aa(main()).then(function (val) {
        console.log('main: finish:', val);
    });

    function *main() {
        var result;
        console.log('main: started');

        var chan = Channel();

        yield [
            function *() {
                console.log('main:', yield sleep(100, 'a1'));
                console.log('main:', yield sleep(200, 'a2'));
                chan('a1-a2');  //----------------------------------+
                console.log('main:', yield sleep(300, 'a3')); //    |
            },                                                //    |
            function *() {                                    //    |
                console.log('main:', yield sleep(200, 'b1')); //    |
                console.log('main:', yield chan, '-> b'); // <------+
                console.log('main:', yield sleep(200, 'b2'));
            }
        ];

        return 'return value!';
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

繰り返しイベントが発生する様なものは? ストリームは? インターバルタイマーは?

npm aaaa.Channel を使うと自由に繰り返し同期できる。

ストリームは...

es2015-stream-ex.js
    var fs = require('fs');
    var aa = require('aa'), Channel = aa.Channel;

    aa(function *main() {
        var chan = Channel().stream(fs.createReadStream('es2015-stream-ex.js', {encoding: 'utf8'}));
        var writer = fs.createWriteStream('es2015-stream-ex.log', {encoding: 'utf8'})

        var buff;
        while (buff = yield chan)
            writer.write(buff);
        writer.end();
    });

インターバルタイマーは...

es2015-interval-ex.js
    var aa = require('aa'), Channel = aa.Channel;

    aa(function *main() {
        var chan = Channel();

        var countDown = 10;
        var interval = setInterval(function () { chan(countDown--); }, 100);

        var val;
        while (val = yield chan)
            console.log('count:', val);

        clearInterval(interval);
    });

あまりにも並列度が多過ぎて過負荷になる事を避けたい時は? 並列度に制限を持たせたい。

npm executors を使うと良い。
並列処理だとすぐに終わるが、大量にリソースを食うものだと、問題になる事がある。
例えばDBアクセスとか、プロセス起動などだ。
並列度がはっきりとわかっている場合は、まだ良いが、多数のクライアントからの要求は、
並列度の制限を設けたい場合もあるのだ。

9並列になってしまう場合に3並列に抑えたいという時は、並列度に制限をかけたい所に
var executors3 = Executors(3); などとして、yield executors3(fn, args,...); とすると良い。
同時に3並列以上実行しない様にできる。

es2015-executors-ex.js
    var aa = require('aa'), Channel = aa.Channel;
    var Executors = require('executors');

    aa(function *main() {
        var parallel9 = [];
        console.time('parallel9');
        for (var i = 0; i < 9; ++i)
            parallel9.push(sleep(100, i));
        yield parallel9;
        console.timeEnd('parallel9');

        var executors3 = Executors(3);
        var parallel3 = [];
        console.time('parallel3');
        for (var i = 0; i < 9; ++i)
            parallel3.push(executors3(sleep, 100, i));
        yield parallel3;
        console.timeEnd('parallel3');
    });

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

その他 promisify, thunkify など

aa.promisify(), aa.thunkify(), aa.promisifyAll(), aa.thunkifyAll() などを駆使すれば、結構いろんな事ができる。

var fs = require('fs');

// 単独のメソッドを aa で yield できる様にする。
aa.promisify(fs, 'exists', {suffix: 'A'});
// または
aa.thunkify(fs, 'exists', {suffix: 'A'});

// オブジェクトのメソッド群を aa で yield できる様にする。
aa.promisifyAll(fs, {suffix: 'A'});
// または
aa.thunkifyAll(fs, {suffix: 'A'});

if (yield fs.existsA('file.txt')) {
    var buff = yield fs.readFileA('file.txt', {encoding: 'utf8'});
}

// 例えば postgres の場合は、以下の様な感じだ。
var pg = require('pg');
aa.promisifyAll(pg.constructor.prototype);
aa.promisifyAll(pg.Client.prototype);
// etc...

参考文献

以下のリンク先も参考にして欲しい。

[JavaScript] 非同期処理のコールバック地獄から抜け出す方法 - Qiita

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした