2011年12月13日

このコールバック地獄からの卒業

 JavaScriptの名前システムに日々キレている読者諸氏よこんにちは。もし私がタイムマシンで1994年に戻って「オサマ・ビン・ラディンを暗殺するか、それともネットスケープ社を爆破するか、どちらかを選べ」と迫られたら間違いなく後者を選ぶ。迫られなくても選ぶ。
 Tiny BASIC以来最悪と名高い(高めるのは私)JavaScriptの名前システムで誰もがまず困るのは、コールバック・スパゲィだ。それがどんなものか知らない幸運なかたはこちらを参照。よい名前システムは七難隠すで、多少のコールバック・スパゲィごときはどうにでもなる。よほどの場合でなければ同期オブジェクトを持ち出してもいい。が、JavaScriptにはどちらもない。
 そこで、世の中ではすでに山のようにコールバック・スパゲィ対策が打ち出されているが、デファクトスタンダードはない。
 私もひとつ対策を打ち出した。最初は、あまりの黒魔術ぶりに我ながら恐れをなして公表を控えようと思っていたが、世の中にすでにこんなにたくさん対策があるなら私がひとつくらい増やしたところでどうということもあるまい、と思い直し、ここに公表する。
 例1:


var sq = new Sequence([
	function () {
		alert('run 1');
		this.a = 1;
		setTimeout(this.$next, 1);
		return true;
	},
	function () {
		alert('run 2 this.a:' + this.a);
		this.$next('OK');
		return true;
	},
	function (arg) {
		alert('run 3 arg:' + arg);
	},
	function () {
		alert('never');
	}
]);

sq.Start();

 例1でのルールは簡単、
・関数オブジェクトを実行したい順番にArrayに並べる
・関数オブジェクト内からは、自分の次の関数オブジェクトがthis.$nextで見える
・this.$nextはsetTimeoutに渡されても$next内でthisを復元する
・次の関数オブジェクトを実行する処理を行ったときはtrueを返す
 とりあえず誰でもこれくらいはやるだろう。黒魔術はここから始まる。
 例2:


var sq = new Sequence([
	function () {
		alert('run 1');
		setTimeout(this.$1, 1);
		return true;
	},
	[
		function () {
			alert('run 2');
		}
	],
	function () {
		alert('run 3');
	}
]);

sq.Start();

 というわけで、
・関数オブジェクトを並べるArrayは入れ子にできる
・入れ子内の関数オブジェクトは、Arrayの前の関数オブジェクトからthis.$1、$2、...$nで見える
・入れ子内の関数オブジェクトでtrueを返さないと、外の次の関数オブジェクトに戻る
 ちなみに、
・入れ子内の関数オブジェクトからは外の次の関数オブジェクトがthis.$parentで見える。
 入れ子ができるので、こういうこともできる。例3:


var inner =	[
 	function () {
 		alert('run 2 this.a:' + this.a);
 		setTimeout(this.$next, 1);
 		return true;
 	},
 	function () {
 		alert('run 3');
 		this.$parent("OK");
 		return true;
 	}
 ];

 var sq = new Sequence([
 	function () {
 		alert('run 1');
 		this.a = 1;
 		setTimeout(this.$1, 1);
 		return true;
 	},
 	inner,
 	function (arg) {
 		alert('run 4 arg:' + arg);
 		var that = this;
 		setTimeout(function() {
 			that.$1();
 		}, 1);
 		return true;
 	},
 	inner,
 	function (arg) {
 		alert('run 5 arg:' + arg);
 	}
 ]);

 だいぶ黒くなってきた。
 タイムアウトとエラー処理と終了処理もあるんだぜ。


var sq = new Sequence([
	function () {
		alert('run 1');
		setTimeout(this.$next, 1);
		return true;
	},
	function () {
		alert('run 2');
	}
], function() {
	alert('onexit');
}, function() {
	alert('onerror');
}, 1000, function() {
	alert('ontimeout');
});

 ちなみにエラー処理はthis.$onErrorで見える。
 さて警告は十分したと思うので、実際に動くコードを以下に載せて終わる。


function Sequence(funcArray, onExit, onError, timeout, onTimeout) {
	this.fa = funcArray;
	this.onExit = onExit;
	this.onError = onError;
	this.timeout = timeout;
	this.onTimeout = onTimeout;
	this.contextSet = {};
	this.contextSetIndexCounter = 0;
}
Sequence.prototype._caller = function(f, context, args, loc) {
	if (context.$_isTimeouted || context.$_isExited) {
		return;
	}
	var lf = f;
	var lloc = loc;
	while (true) {
		this._prepareCall(lf, context, this.fa, null, lloc, 0, []);
		context.$self = this._generateFunc(lf, context, lloc);
		context.$_lastPassedTime = new Date().getTime();
		if (lf.apply(context, args)) {
			return;
		} else if (context.$_parent) {
			lf = context.$_parent;
			lloc = context.$_parentLoc;
		} else {
			break;
		}
	}
	if (context.$onExit) {
		context.$onExit();
	}
};
Sequence.prototype._generateFunc = function(f, context,  loc) {
	var that = this;
	return function() {
			that._caller(f, context, arguments, loc);
	};
};
Sequence.prototype._prepareCall = function(f, context, cfa, pf, loc, depth, ploc) {
	for (var i = loc[depth]; i < cfa.length; i ++) {
		var a = cfa[i];
		if (a === f) {
			delete context.$next;
			var c = 1;
			while (context['$' + c]) {
				delete context['$' + c];
				c++;
			}
			if (pf) {
				context.$_parent = pf;
				context.$_parentLoc = ploc;
				context.$parent =  this._generateFunc(pf, context, ploc);
			} else {
				delete context.$_parent;
				delete context.$parent;
			}
			for (var ii = i + 1; ii < cfa.length; ii ++) {
				var nf = cfa[ii];
				if (typeof(nf) == 'function') {
					var nloc = loc.concat();
					nloc[depth] = ii;
					context.$next =  this._generateFunc(nf, context, nloc);
					break;
				}
			}
			if (i  + 1 < cfa.length) {
				var n = cfa[i + 1];
				if (typeof(n) != 'function') {
					var c = 1;
					for (var ii = 0; ii < n.length; ii ++) {
						if (typeof(n[ii]) != 'function') {
							continue;
						}
						var cf = n[ii];
						var cfloc = loc.concat([ii]);
						cfloc[depth] = i + 1;
						context['$' + c] = this._generateFunc(cf, context, cfloc);
						c ++;
					}
				}
			}
			return true;
		}
		if (typeof(a) != 'function') {
			var lpf = pf;
			var lploc = ploc;
			if (i  + 1 < cfa.length) {
				lpf = cfa[i + 1];
				lploc = loc.slice(0, depth).concat([i + 1]);
			}
			if (this._prepareCall(f, context, a, lpf, loc, depth + 1, lploc)) {
				return true;
			}
		}
	}
};
Sequence.prototype.Start = function(context) {
	var that = this;
	if (!context) {
		context = {};
	}
	context.$_isExited = false;
	context.$_lastPassedTime = null;
	context.$_isTimeouted = false;
	context.$IsTimeouted = function(now) {
		if (context.$_isTimeouted || context.$_isExited) {
			return true;
		}
		if (that.timeout) {
			if (that.timeout + context.$_lastPassedTime < now) {
				context.$onTimeout();
				if (context.$onExit) {
					context.$onExit();
				}
				context.$_isTimeouted = true;
				return true;
			}
		}
		return false;
	};
	context.$onExit = function() {
		delete context.$onExit;
		if (that.onExit) {
			that.onExit.apply(context, arguments);
		}
		context.$_isExited = true;
		delete that.contextSet[context.$_index];
	};
	context.$onError = function() {
		if (that.onError) {
			that.onError.apply(context, arguments);
		}
		context.$onExit();
	};
	context.$onTimeout = function() {
		if (that.onTimeout) {
			that.onTimeout.apply(context, arguments);
		}
		context.$onExit();
	};
	var f = this.fa[0];
	context.$_index = this.contextSetIndexCounter;
	this.contextSetIndexCounter ++;
	this.contextSet[context.$_index] = context;
	this._caller(f, context, [], [0]);
};

var inner =	[
	function () {
		alert('run 2');
		alert(this.a);
		setTimeout(this.$next, 1);
		return true;
	},
	function () {
		alert('run 3');
		var that = this;
		setTimeout(function() {
			that.$parent("OK");
		}, 1);
		return true;
	}
];

var sq = new Sequence([
	function () {
		alert('run 1');
		this.a = 1;
		setTimeout(this.$1, 1);
		return true;
	},
	inner,
	function (arg) {
		alert('run 4 arg:' + arg);
		var that = this;
		setTimeout(function() {
			that.$1();
		}, 1);
		return true;
	},
	inner,
	function (arg) {
		alert('run 5 arg:' + arg);
		this.$onError();
		return true;
	}
], function() {
	alert('onexit');
}, function() {
	alert('onerror');
}, 10000, function() {
	alert('ontimeout');
});

function Tick() {
	var now = new Date().getTime();
	for (var i in sq.contextSet) {
		var ci = sq.contextSet[i];
		if (ci.$IsTimeouted(now)) {
			alert("timeouted");
		}
	}
}
setInterval(Tick, 500);

sq.Start();

Posted by hajime at 2011年12月13日 14:24
Comments