javascript 再帰による簡単な同期処理の方法

javascriptでコードを書いていると、同期処理を必要とする場面に出くわした。

デフォルトだと、非同期処理となりうまく実行されなかったのだ。

ピンとこない人に簡単に説明をすると、同期処理とは「その実行が完了するまで次の行に進まない」、
非同期処理とは「実行の終了を待たずに次の実行へ進む」というものである。

非同期処理のほうがスピードも速くていいような気がするが、プログラムによっては迷惑極まりないという場合もある。
関数内でsetIntervalを使ったときに問題が発生したので、それを頭に入れて考えてほしい。

再帰による同期処理

まず、いろいろと調べてみた結果、同期処理というのは難しくなかなか頭に入ってこなかった。

その中でも一番シンプルであろう「コールバック地獄」という方法を選択した。考え方自体はシンプルであるが、コードの量が莫大に増えるというプログラマーにとっては地獄のようなアルゴリズムということでこんな名前になったらしい。

とりあえず、一番最初、筆者が何も知らなかったときに書いたコードを記述する。目標は「配列内の処理を順に実行すること」である。
この場合「111222333」と出力されればokだ。

html


<!DOCTYPE html>
<html>
<head>
<title>再帰による同期処理</title>
</head>
<body>
<div onclick="go();">
クリックでスタート
</div>

<script type="text/javascript" src="./sample.js"></script>
</body>
</html>

js


var lists=[];	//命令を格納する配列を宣言

lists[0]=function(){
	var count=0;
	var interval=setInterval(function(){
		console.log(1)
		if(count==2){
			clearInterval(interval);
			count=0;
		}	
		count++;
	},10);
};
lists[1]=function(){
	var count=0;
	var interval=setInterval(function(){
		console.log(2)
		if(count==2){
			clearInterval(interval);
			count=0;
		}	
		count++;
	},10);
};
lists[2]=function(){
	var count=0;
	var interval=setInterval(function(){
		console.log(3)
		if(count==2){
			clearInterval(interval);
			count=0;
		}	
		count++;
	},10);
}

function go(){
	for(var i=0;i<3;i++){
		lists[i]();
	}
}

jsの方は若干ややこしく見えるかもしれないが、実際は結構簡単なプログラムである。

lists[]の中もsetIntervalではなくてfor文を使えばいいじゃないかという声もあるが、これは簡略化したプログラムであるので、実際はsetIntervalを使わなければいけない事情があった。

この場合出力結果は「123123123」となった。

もちろんgo関数の中を


lists[0]();
lists[1]();
lists[2]();

としても結果は同じである。


次にコールバック地獄のプログラムを記述する。htmlは変化しない。


function go(){
	lists[0]();
	setTimeout(()=>{
		lists[1]();
		setTimeout(()=>{
			lists[2]();
		},100);
	},100);
}

変える部分はgo関数の中のみでかまわない。

setTimeoutを使い、処理が終わるまで待ってもらうというプログラムだ。
正確には「処理が終わるまで」ではなく、「100ミリ経過するまで」という意味であるので、それ以上の処理が要求される場合はここの時間を長くする必要がある。

結果としては「111222333」と望むものが出てきた。

しかし今回はlistsの配列の長さが3しかなかったから良かったものの、これが10や100になってしまった場合、大変な量を書くことになる。
これが地獄たるゆえんだろう。

そこで私の頭の中に、画期的なアイディアが思い浮かんだ。


タイトルでもある通り、再帰を利用した処理である。

インターネットを探せば、どこかしらには存在したであろう情報かもしれないが、残念ながら見つけることはできなかったので自分で考えるしかなかった。


function go(){
	var i=0;
	(function move() {
		if(i<lists.length) { lists[i](); i++; setTimeout(()=>{
				move();
			},100);
		}
	}());
}

結果はもちろん「111222333」だ。

これを思いついた時には本当に自分に感動した。軽く小躍りしたまである。さらに、個人的なプログラムなのでソースコード内に

/*–このプログラム神–*/

と記してしまった。

簡単に説明すると、処理全体を即時関数化してしまい、それを再び呼び出すという方式である。

呼び出す回数をlistsの数、すなわちlists.length回に設定することで無駄なくループされるということだ。

まとめ

このプログラムの脆弱性は、先ほども書いた

正確には「処理が終わるまで」ではなく、「100ミリ経過するまで」という意味であるので、それ以上の処理が要求される場合はここの時間を長くする必要がある。

という点だ。

コールバックやら、プロミスやらが分かる人はそっちを利用したほうが良いソースコードとなるはずだ。

コールバック地獄に陥りそうな人は最終手段としてこれを利用してほしい。