yieldを使って非同期処理を順番に実行する
JavaScriptはシングルスレッドで動作するので、ネットワーク処理などが非同期で行われるようになっています。そうしないとAjaxを使っている間、何も入力や画面スクロールなども受け付けなくなります。
しかし非同期実行のしわ寄せとして、コールバックが多くなってネストが深くなったり、Promiseでも処理が分かりづらくなります。例えば非同期処理を伴ったループ処理を書こうとすると、次のようになります。
loop([1, 2, 3, 4, 5, 6], function(key, value, promise) {
value = value * 2;
promise.resolve(value);
}).then(function(results) {
console.log(results);
})
function loop(ary, call) {
var results = {};
return new Promise(function(resolve, reject) {
var keys = Object.keys(ary);
var index = 0;
function each_loop(key, value, index) {
if (typeof key == 'undefined') {
return resolve(results);
}
return new Promise(function(res, rej) {
call(key, value, {resolve: res, reject: rej})
}).then(function(msg) {
results[key] = msg;
index++;
each_loop(keys[index], ary[keys[index]], index);
},
function(msg) {
return reject(msg);
});
}
each_loop(keys[index], ary[keys[index]], index);
});
}
/*
$ node promise_test.js
{ '0': 2, '1': 4, '2': 6, '3': 8, '4': 10, '5': 12 }
*/
他のプログラミング言語に比べてとても難解なものになってしまいがちです。
そこで使ってみたいのがyieldです。JavaScript 1.7からの構文になりますが、多くのブラウザでサポートが開始されています。
yieldの使い方
簡単な使い方を紹介します。Google ChromeのDevToolsなどで試せます。
まず最初にジェネレータを生成します。特徴は function*
ではじまることです。
function* generator(){
yield 1;
yield 2;
yield 3;
yield 4;
}
そしてこのジェネレータを実行します。そうするとジェネレータオブジェクトが生成されます。
g = generator();
// generator {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
そうするとすぐにyieldが実行される訳ではなく、停止した状態になっています。実行するには next() メソッドを使います。
g.next();
// Object {value: 1, done: false}
valueはyieldの返却値です。doneがfalseになっているのが分かります。つまり、まだyield文があるということです。さらに実行します。
g.next();
// Object {value: 2, done: false}
g.next();
// Object {value: 3, done: false}
g.next();
// Object {value: 4, done: false}
g.next();
// Object {value: undefined, done: true}
4まで終わるとdoneがtrueとなって返ってきます。これが基本的な使い方です。
yieldを使った処理について
基本的な使い方が見た通り、doneのステータスを見ながらfalseであれば次のyieldを実行していくようにすれば非同期処理が順番に実行できるようになります。まずジェネレータは次のようになります。
function* generator() {
yield new Promise(function(resolve, reject) {
console.log("Execute after 1000ms")
setTimeout(function() {resolve("1000ms waited.")}, 1000);
});
yield new Promise(function(resolve, reject) {
console.log("Execute after 3000ms")
setTimeout(function() {resolve("3000ms waited.")}, 3000);
});
yield new Promise(function(resolve, reject) {
console.log("Execute after 500ms")
setTimeout(function() {resolve("500ms waited.")}, 500);
});
}
そして生成されたジェネレータをwhileでループ処理します。
var g = generator();
function Loop(g) {
var p = g.next();
if (p.done) return;
p.value.then(function(msg) {
console.log(msg);
Loop(g);
});
}
Loop(g);
そうすると正しい順番で 1000ms、3000ms、500ms停止してから処理を実行します。大事なのはPromiseオブジェクトを返すようにするという点と、ループ処理部分はジェネレータを再帰的に呼び出す必要があるので関数で囲まなければならないということです。
yieldを使うことで、非同期処理がネストせず、同期処理のように書けます。コードの見通しが格段に良くなりますので、不具合の発生が抑えられるようになるでしょう。Webアプリケーションなどで非同期処理の発生が見込まれる場合、元々全体を関数で囲んでしまっておいても良いかもしれません。
なお、yield自体はまだ一部のブラウザでサポートされていませんので、その場合はBabelを使って変換するのが良さそうです。
コメントは受け付けていません。