JavaScriptで,重いループ処理を軽く非同期で実行できるライブラリ ver0.1


以前,ブラウザ上ですごく重いテキスト処理を実行した時に,

ササッと作ったライブラリがあった。

今回,それを改良して独立させた。

複数行のテンプレートマッチング繰り返しで,項目を一覧表形式で抽出するJSコード (改良版) - ソフトウェア勉強ログとサンプルコード
http://source-code-student.hatenablog.jp/entry/20150110/p1

  • 配列の各行に対してすごく重い処理をしたい


下記はサンプルコード。 ====

サンプルコード

<script src="HeavyLoop.js"></script>

<h1>HeavyLoop.jsを使って,<br>
ループ構文や配列イテレータが重くても,<br>
フリーズせずにサクサク動作するサンプル</h1>


<h2>重いforループを,軽く動かす</h2>

<pre>

heavy_for(
  初期値,
  function(i){ return 上限 },
  function(i){ return インクリメント処理 },
  function(i){
    〜繰り返し処理〜
  }
);

</pre>

<input type="button" value="スタート" onclick="f1()"><br>
<br>

<textarea cols="80" rows="2" id="txt1"></textarea><br>
<br>



<h2>重いwhileループを,軽く動かす</h2>

<pre>

heavy_while(
  function(){ 〜継続の条件〜 }, 
  function(){
    〜繰り返し処理〜
  }
);

</pre>

<input type="button" value="スタート" onclick="f2()"><br>
<br>

<textarea cols="80" rows="2" id="txt2"></textarea><br>
<br>



<h2>重いeachループを,軽く動かす</h2>

<pre>

[1,2,3, …, 1000]
  .heavy()
  .each(function(item, index){
    〜配列の各要素に対する処理〜
  });

</pre>

<input type="button" value="スタート" onclick="f3()"><br>
<br>

<textarea cols="80" rows="5" id="txt3"></textarea><br>
<br>



<script>

function f1(){

	// 重いforループを軽く実行
	heavy_for(
		0, // 初期値
		function(i){ return i < 10000; }, // 上限
		function(i){ return ++i; }, // インクリメント
		function(i){
			// 画面上に数字を表示
			$("txt1").value = i;
		}
	);

}


function f2(){

	var i = 0;

	// 重いwhileループを軽く実行
	heavy_while(
		function(){ return i < 10000; }, // 継続条件
		function(){
			// 画面上に現在時刻ミリ秒を表示
			$("txt2").value = new Date().getTime();
			
			i ++;
		}
	);

}


function f3(){

	(1).upTo(1000) // 数字の並んだ配列
	
		.heavy() // 配列に対して「この配列は重いぞ!」と宣言しておく
		
		.each(function(item, index){ // 通常の配列のようにeachでイテレータ
			
			// 画面上に数字を表示
			$("txt3").value = 
				"["
				+ index
				+ "]"
				+ item 
				+ ", " 
				+ $("txt3").value
			;
			
		})
		.afterThat(function(){
		
			$("txt3").value = "ループが完了しました。"
				+ "ちなみに,この配列の長さは"
				+ this.length
				+ "です。"
				+ $("txt3").value;
			;
		})
		.run() // 上記のループ処理全体を実行
	;
	
}


function $( dom_id ){
	return document.getElementById( dom_id );
}

function log(s){
	console.log(s);
}


// 整数の配列を生む
Number.prototype.upTo = function(max){
	var arr = [];
	
	// 本当はreduce()を使って実装すべきだが,
	// 実装依存を少なくするためにfor文で。
	for( var i = this; i <= max; i++ ){
		arr.push( i );
	}
	
	return arr;
};

</script>


<style>

pre{
	margin : 10px;
	padding : 10px;
	background-color : gainsboro;
}

body{
	padding : 10px;
}


</style>


HeavyLoop.js

/*

	HeavyLoop.js 
	〜ブラウザ上で,重いループを軽く動かすライブラリ〜

	@author id:SourceCode-Student
	
	ver 0.1, MIT License

*/



// -------- 制御構文の代替表現 -------- 


// まあ単なるsetTimeoutの繰り返しなんだけどね・・・。


// 重いforループ
function heavy_for( cnt_init, func_cond, func_incr, func_loop ){

	// iに初期値を代入
	var i = cnt_init;
	
	// while文を使ってfor文を実装する
	heavy_while(
		function(){
			// iが条件を満たす間は継続
			return func_cond( i ); 
		},
		function(){
			// ループ処理を一回分実行
			func_loop( i );
			
			// カウンタ変数を次に進める
			i = func_incr( i );
		}
	);
}


// 重いwhileループ
function heavy_while( func_cond, func_loop ){

	// 1回分のループ処理
	var one_loop_while = function( cnt ){
		
		// 継続条件を満たしていれば
		if( func_cond() ){
			
			// 今回の分
			var p = new PromiseTask(function(){
				// タスク登録
				func_loop();
			})
			.afterThat(function(){
				// 今回が終わったら次のループへ
				one_loop_while();
			})
			.execTask() // 今回の分を実行
			;
		}
		
	};

	// 初回から着手
	one_loop_while();
}



// -------- Promiseタスク -------- 


// 動作の約束オブジェクト
var PromiseTask = function( func, args, params ){
	//log( "promise生成" );
	
	this._func = func;
	this._args = args;
	
	if( params ){
		if( params.get_param_from_prev_step ){
			//log("このpromiseは実行時に直前のpromiseから値を受け取ります。");
			this.get_param_from_prev_step = true;
		}
	}
}
PromiseTask.prototype = {
	_func : null,
	_args : null,
	
	_prev_step : null,
	_next_step : null,
	
	get_param_from_prev_step : false,
	
	// 次の動作を決めておく
	setNextStep : function( p ){
		this._next_step = p;
		p._prev_step = this;
		
		return this;
	},
	
	// 次の動作をクロージャで指定
	afterThat : function( func ){
		var p = new PromiseTask( func, [] );
		this.setNextStep( p );
		
		return this;
	},
	
	// ここまでのプロミスチェーンを全部実行
	execAllChain : function( next_func ){
		var p = this; // 基点となる自分自身=最後のタスク
		var p_arr = [];
		
		// 自分自身から前の動作へたどってゆく
		while( p != null ){
			p_arr.push( p );
			p = p._prev_step;
		}
		
		// 時系列に並び替える
		p_arr = p_arr.reverse();
		
		if( next_func ){
			// 最後に付与
			var p_last = new PromiseTask( 
				next_func, 
				[], 
				{
					// 直前の返り値を実行引数とする
					get_param_from_prev_step : true 
				} 
			);
			p_arr[ p_arr.length - 1 ].setNextStep( p_last );
		}
		
		// 先頭のプロミスを取得して実行
		p_arr[0].execTask();
		
		return this;
	},

	// このプロミスを実行
	execTask : function( arg ){
		
		// 前回の結果を引き継ぐか?
		if( this.get_param_from_prev_step ){
			// 引数として,前回の返り値を優先する。
			this._args = [ arg ];
			//log("前回の結果を引き継ぐために,引数を更新しました。: " + this._args );
			//log("更新済みの引数の個数:" + this._args.length );
		}
	
		// 動作を実行
		//log("promise実行します・・・");
		var result = this._func( this._args );
			//log("promise実行しました。実行結果:" + result );
		
		var next_p = this._next_step;
		if( next_p ){
			// 非同期に次へ移る
			setTimeout( function(){
				next_p.execTask( result )
			}, 0 );
		}
	}
	
};


// -------- 配列を拡張して,重い処理に耐えられるようにする -------- 


// 普通の配列をHeavyArrayに変換
Array.prototype.heavy = function(){
	return new HeavyArray( this );
};


// 重い処理に耐えうる配列
var HeavyArray = function( normal_arr ){
	this._arr = normal_arr;
};
HeavyArray.prototype = {
	// 内部で通常の配列を保持
	_arr : null,

	_last_task : null,

	// イテレータ
	each : function(func){
		
		// promiseを数珠つなぎにする(map()を使わずに実装)
		var promises = [];
		var _arr = this._arr;
		var p;
		for( var i = 0; i < _arr.length; i ++ ){
			p = new PromiseTask(
				function( args ){
					//log("each内");
					func.apply( 
						_arr, // 処理内では配列がthis
						[ args[0], args[1] ] // 要素とインデックスを渡す
					);
				},
				[ _arr[i], i ] 
			);
			promises.push( p );
		}
		for( var i = 1; i < _arr.length; i ++ ){
			// 後続タスクとして登録
			promises[ i - 1 ].setNextStep( promises[ i ] );
		}
	
		// 末尾のpromise要素だけを保持
		this._last_task = promises[ promises.length - 1 ];
		
		return this;
	},
	
	// 末尾に処理を付与
	afterThat : function( func ){
		// タスク生成
		var _arr = this._arr;
		var p = new PromiseTask(function(){
			func.apply( _arr );
		});
		
		// 後続として登録
		this._last_task.setNextStep( p );
		
		// 末尾を更新
		this._last_task = p;
		
		return this;
	},
	
	// 全体を実行
	run : function(){
		this._last_task.execAllChain();
		
		// 全部終わったら,何事も無かったかのように普通の配列に戻る
		return this._arr;
	}
};

工夫した点

  • forやwhileを非同期化するライブラリは既存のものが存在するが,一歩進んで,配列のイテレータにそれを組み入れたというのが面白いところ。


今後の課題:

  • map()とかfilter()とか,さまざまなイテレータを非同期処理できるようにしたい。
  • 整数問題をブラウザ上でJavaScriptで解く際に,すごく重い探索処理であっても,このライブラリを使って非同期でサクサク実行できるようにしたい。
  • WSH/JScriptで実行させた時に,自動的にsetTimeout不使用の挙動に切り替えたい。コンソールで実行している分には,重いループ処理でも関係なく実行できるので。