ES2017 async/await
+ Promise
で解決できる事
ES2017 async/await
と Promise
を使うと非同期処理をすごく簡単に処理できる。
とても便利なのだが、それだけでは、どうも機能が足りない様に見える。
この記事は... TL;DR
ES2017 async/await を使っても、まだいろいろと課題は残ってるよ。
ES2015 (ES6) generators と npm aa (async-await) だと、より良い解決策があるよ。って話。
以下の図の様な非同期処理フローを考えてみる。
横軸は時間だ。左から右へ時間が流れていくものと考えて欲しい。
縦線は複数の処理を同期させたい、という意味だ。
ES2017 async/await
版のコード例を見てみよう。
'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 (ES6) generators (yield) 版のコード例を見てみよう。
// 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 *()
にして、await
を yield
にすれば、ほぼ同じコードだ。
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 を使わなくても良い
<!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 を使ってください。
合流せずに途中結果だけを同期したかったら?
以下の図の様な非同期処理フローを考えてみる。
npm aa の aa.Channel
を使うと自由に同期できる。yield chan;
とすると同期できる。
処理を分割(fork)して合流(join)したい時、generatorの配列を使うと良い。
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 aa の aa.Channel
を使うと自由に繰り返し同期できる。
ストリームは...
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();
});
インターバルタイマーは...
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並列以上実行しない様にできる。
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]
(https://qiita.com/LightSpeedC/items/7980a6e790d6cb2d6dad)