2015年10月11日

JavaScriptは如何にしてAsync/Awaitを獲得したのか

JavaScriptを記述する上で、避けて通れないのが非同期処理。

人類は、長い年月、この非同期的な処理を「如何にして書きやすく、読みやすく記述するか」について探求してきました。

要するに†闇†の塊なのですね。(闇に飲まれよ!!!!)

この物語は、そんな†闇†の存在だった非同期処理を、人類がどのように苦しみ、そしてどのように解決していったかを書いていくポエムである。


補足:厳密には、JSはシングルスレッドで動くため、非同期処理は存在しない!と言ってしまえばそこまでなのですが、今回はsetTimeoutやajax通信、onloadイベント登録など、見かけ上、非同期的な挙動を示すものすべてを対象に話していきます。


第一章 〜人類はsetTimeoutを採用しました〜

古代のJavaScriptで、以下のような処理をしたい場合、どうしていただろうか。

「ブラウザ更新直後に『a』を表示し、その2秒後に『b』を表示し、更にその1秒後に『c』を表示する」

これの回答には、以下のような例も考えられるだろう(真似しないでください)

var oldDate = new Date();

//更新後、即表示
console.log("a");

//2秒間すぎるまで無限ループ
while((new Date() - oldDate) < 2000);
oldDate = new Date();

console.log("b");

//1秒間すぎるまで無限ループ
while((new Date() - oldDate) < 1000);
oldDate = new Date();

console.log("c");
しかし、これには非常に問題があるというのがお分かりいただけるだろうか。

というのも、JavaScriptはシングルスレッドである。
従って、このwhileループを回している間、他のいかなる処理も処理できないのだ。
(それとおまけ程度に言えば、こんなことのためにCPU燃やすのはやめて差し上げろ)

その為、中世の人類はこれに対処するために、まず最初にsetTimeoutを産んだ。
これが最大級の救済であり、と同時にその後何十年間も人々を苦しめる災いとなったのだ。

//更新後、即表示
console.log("a");

setTimeout(function(){
console.log("b");
setTimeout(function(){
console.log("c");
},1000);
},2000);
setTimeoutは、第一引数にコールバック関数、第二引数に処理遅延するミリ秒を記述する。
コールバック関数ってなんだよ、って思うかもしれないが、ここでは「第二引数に指定したミリ秒後に、実行させたい関数を渡す」程度で十分だろう。



第二章 〜setTimeoutが生み出した悲劇〜

さて、これで動作上は問題が無くなった。 "理論上は" どんなコードでも書くことが出来るはずだ。
しかし、まだ気になることがないだろうか。

そう、「ネストが深くなってしまった」のだ。

今回は簡単な例だったから良いが、実際の開発とものあると、この手法で開発を続けると、深いネストに包まれて、最悪の場合死に至るのだ。

内容は理解しなくていい、こういうことになるのだ、その雰囲気だけ掴んで欲しい。

window.addEventListener("load",function(eve){
$.ajax({
src: "/json/job.json",
dataType: "json",
}).done(function(job){
$.ajax({
src: "/json/skill.json",
dataType: "json",
}).done(function(skill){
$.ajax({
src: "/json/status.json",
dataType: "json",
}).done(function(status){
//最初3秒間アニメーションをする
var timerId = setInterval(function(){
//アニメーションの実装を以下に書いていく…
var ctx = $("canvas")[0].getContext("2d");
//(以下略)
},100);

setTimeout(function(){
//以下に本体処理を入れていく…
clearInterval(timerId);
//(以下略)
},3000);
}).fail(function(err){
console.log("読み込みエラーです");
});
}).fail(function(err){
console.log("読み込みエラーです");
});
}).fail(function(err){
console.log("読み込みエラーです");
});
},false);
はい、どうかんがえても闇ですね。

というわけで、次に人類は「オレオレ非同期ライブラリ」を作り始めたわけですが、
この辺の話をすると、流石に収集つかなくなるのでこの辺りは省略します。



第三章 〜非同期処理機構、Promise〜

その後、人類が近世に入ってから、「Promise」という新しい非同期処理機構が生まれた。

Promiseの説明については Promiseが実装された - JS.next などが理解しやすいと思います。
もちろん「JavaScript Promise」などでググって学習しても構いません。

そして、このPromiseを用いると、ネストが劇的に軽減されました。革命です。パラダイムシフトです。

new Promise(function(resolve){
window.addEventListener("load",function(eve){
resolve([]);
},false);
}).then(function(arr){
return new Promise(function(resolve,reject){
$.ajax({
src: "/json/job.json",
dataType: "json",
}).done(function(data){
arr.push(data);
resolve(arr);
}).fail(function(err){
reject("job");
});
});
}).then(function(arr){
return new Promise(function(resolve,reject){
$.ajax({
src: "/json/skill.json",
dataType: "json",
}).done(function(data){
arr.push(data);
resolve(arr);
}).fail(function(err){
reject("skill");
});
});
}).then(function(arr){
return new Promise(function(resolve,reject){
$.ajax({
src: "/json/status.json",
dataType: "json",
}).done(function(data){
arr.push(data);
resolve(arr);
}).fail(function(err){
reject("status");
});
});
}).catch(function(err){
console.log(err + "が取得できませんでした");
}).then(function(arr){
return new Promise(function(resolve,reject){
//最初3秒間アニメーションをする
var timerId = setInterval(function(){
//アニメーションの実装を以下に書いていく…
var ctx = $("canvas")[0].getContext("2d");
//(以下略)
},100);
setTimeout(function(){
clearInterval(timerId);
resolve(arr);
},3000);
});
}).then(function(arr){
//ここに本体処理を書いていく
//...
//(以下略)
});
Promiseの登場により、人類は "理論上" どんなに非同期ネストが深くなっても、ある一定のネストまでで食い止めることが出来るようになりました。

これは比較的現代的で、現行Promiseを利用している人も多く、十分実用的であります(これめっちゃポエムっぽい言い回し)



第四章 〜Promiseに潜む闇〜

しかし、最強に見えたPromiseにも致命的な弱点があったのです。
それが、「同期処理も強制的にPromiseに包み込まなければならない」という点です。

最初の例に戻りましょう。

「ブラウザ更新直後に『a』を表示し、その2秒後に『b』を表示し、更にその1秒後に『c』を表示する」

でしたね。

new Promise(function(resolve,reject){
console.log("a");
resolve();
}).then(function(){
return new Promise(function(resolve,reject){
setTimeout(resolve,2000);
});
}).then(function(){
console.log("b");
return Promise.resolve();
}).then(function(){
return new Promise(function(resolve,reject){
setTimeout(resolve,1000);
});
}).then(function(){
console.log("c");
return Promise.resolve();
});
どうでしょうか。
「console.log("a");」や「console.log("c");」の行は同期処理です。

そして、その同期処理しか書いてない部分も、非同期処理に引っ張られて「しかたなく」Promiseで包み込まなければならないのです。

・・・これは流石に面倒くさい、ですよね?

つまり、人類はPromiseによって、見事「読みやすさ」を獲得した。
しかし、同時に「書きやすさ」を失ってしまったのだ。(ここテストに出ます)



第五章 〜ES6の突入とGenerator〜

Promiseが夢見た理想の非同期世界は、あくまでモジュールの殆どが非同期で動いているという、限られた環境のみで真価を発揮すると知ってひどく絶望した人類。

絶望の淵、現代の人類がふと脇道に目を逸らすと、そこにはES6の花が咲いていた。

Promise地獄とも呼ぶべき冗長なPromise表現に疲れ果て、朦朧とする意識の中、あぁ、非同期処理もES6でもっとシンプルに書ければ良いのに、そう人類が思い始めた頃。

・・・そうか、出来るじゃないか。PromiseとES6の夢の共演、Generatorだ…!


function Main(g){
//Generatorを一つすすめる
var p = g.next();

//yieldしきっていたら、実行を止める
if(p.done) return;

//yieldでPromiseオブジェクトが帰ってきてるので、valueを介してthenでつなげる
p.value.then(()=>{
//resolveされたら、(最初よりも一つ分nextされた)Generatorを再帰的に渡す
Main(g);
});
}

//Main関数にGeneratorを渡して実行開始
Main(Gen());

function* Gen(){
//実行時即表示
console.log("a");

//このyieldで2秒間待機してくれる(風に見せかけることが出来る)
yield new Promise((resolve)=>{
setTimeout(resolve,2000);
});

//2秒後に表示
console.log("b");

//このyieldで1秒間待機してくれる(風に見せかけることが出来る)
yield new Promise((resolve)=>{
setTimeout(resolve,1000);
});

//1秒後(実行から合計3秒後)に表示
console.log("c");
}
そうだ、これだ。

yieldでPromiseオブジェクトを返し、then以下の処理を外部に追いやる。

こうすれば、Gen関数の部分だけ見れば「上から下へ」綺麗に処理が流れている。
そして、一見同期的だし、ネストも浅く、そして書きやすい…!


function Main(g,val,isReject){
var p = g.next([val,isReject]);

if(p.done) return;

p.value.then((val)=>{
Main(g,val,false);
}).catch((val)=>{
Main(g,val,true);
});
}

//Main関数にGeneratorを渡して実行開始
Main(Gen());

function* Gen(){
var job,skill,status,hasErr;

yield new Promise(function(resolve){
window.addEventListener("load",function(eve){
resolve();
},false);
});

[job,hasErr] = yield new Promise(function(resolve,reject){
$.ajax({
src: "/json/job.json",
dataType: "json",
}).done(function(data){
resolve(data);
}).fail(function(err){
reject("job");
});
});
if(hasErr){
console.error(job + "の読み込みに失敗しました");
return;
}

[skill,hasErr] = yield new Promise(function(resolve,reject){
$.ajax({
src: "/json/skill.json",
dataType: "json",
}).done(function(data){
resolve(data);
}).fail(function(err){
reject("skill");
});
});
if(hasErr){
console.error(skill + "の読み込みに失敗しました");
return;
}

[status,hasErr] = yield new Promise(function(resolve,reject){
$.ajax({
src: "/json/status.json",
dataType: "json",
}).done(function(data){
resolve(data);
}).fail(function(err){
reject("status");
});
});
if(hasErr){
console.error(status + "の読み込みに失敗しました");
return;
}

//最初3秒間アニメーションをする
yield new Promise(function(resolve,reject){
var timerId = setInterval(function(){
//アニメーションの実装を以下に書いていく…
var ctx = $("canvas")[0].getContext("2d");
//(以下略)
},100);
setTimeout(function(){
clearInterval(timerId);
resolve();
},3000);
});

//ここに本体処理を書いていく
//...
//(以下略)
}
非同期だらけの例でも非常に読みやすい。最高。神。

概ね、文句が無いが、強いていちゃもんを付けるとしたら、「Main関数」の部分だろうか。
thenを処理して、Generatorを再帰的に回してるこの部分だ。



最終章 〜これからのES7とAsync/Await〜

未来に生きている人類は、すでにES7の仕様の一部で遊んでいる。神々の遊びだ。

ES7の仕様の一部にある、Async/Awaitは、上記のGeneratorでやっていることの、シンタックスシュガーだ。
Main関数で記述したような再帰処理を、自前で用意することなく、同等のことを実現してくれる。

async function Main(){
//実行時即表示
console.log("a");

//このawaitで2秒間待機してくれる(風に見せかけることが出来る)
await new Promise((resolve)=>{
setTimeout(resolve,2000);
});

//2秒後に表示
console.log("b");

//このawaitで1秒間待機してくれる(風に見せかけることが出来る)
await new Promise((resolve)=>{
setTimeout(resolve,1000);
});

//1秒後(実行から合計3秒後)に表示
console.log("c");
}

Main();
一番最初の脳筋無限ループの例と見比べて欲しい。完全に一致ではないだろうか。

人類がやりたかった、「脳筋無限ループの例ぐらい単純な構造で」「非同期処理を実現する」究極の進化系、これがAsync/Awaitなのだ。

そして、そのAsync/Awaitは、GeneratorとPromiseという、人類の苦労の歴史を融合させて生まれた。
いわば人類の叡智が、Async/Awaitなのだ。

この記事を読んで、Async/Awaitに少しでも興味が湧いたのなら、ぜひともbabel、browserifyをして試してみて欲しい。

当サイトの ES5なJavaScriptにES6を求めるのは間違っているだろうかBabel で async / await を試す などを参考にすれば、きっと出来るはずだ。

諸君らの快適な非同期処理ライフを願う。
posted by がお at 15:06| Comment(0) | Javascript | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: