Event Driven JavaScript

document.createEventとdispatchEvent、addEventListenerを使ってイベントドリブンに書いたJavaScriptがオレブーム(ただし、IE非対応*1 )なので、軽く紹介してみたいと思います。

具体的には、AutoPatchWork (Google Chrome Dev用のextension)をイベントドリブンで実装しています。
AutoPatchWork.jsが2009/06/11 21:00時点のソース(id:nanto_viのコメントをうけて#を.に変更済み)。シンプルさを重視しているので、324行と短めです(CSSは別ファイルですが)。

window.addEventListener('scroll', check_scroll, false);
window.addEventListener('AutoPatchWork.request', request, false);
window.addEventListener('AutoPatchWork.load', load, false);
window.addEventListener('AutoPatchWork.error', error_event, false);
window.addEventListener('AutoPatchWork.reset', reset, false);

まずこんな感じでイベントを待ち受けておきます。

で、たとえばscroll監視関数はこんな感じです。

function check_scroll(){
	if (loading) return;
	var remain = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)
		 - window.innerHeight - window.pageYOffset;
	if (options.state && remain < options.remain_height)
		dispatch_event('AutoPatchWork.request');
}

規定値よりスクロールしたら、AutoPatchWork.requestイベントを投げます。これを先のaddEventListenerで登録しておいたrequest関数が受け取ります。

function request(){
	loading = true;
	icon.className = 'loading';
	var url = next.getAttribute('href');
	var x = new XMLHttpRequest();
	x.onload = function() {
		if (x.status <= 200 && x.status < 300) {
			dispatch_event('AutoPatchWork.load',{response:x,url:url});
		} else {
			dispatch_event('AutoPatchWork.error',{message:'request failed. status:' + x.status});
		}
	};
	x.open('GET', url, true);
	x.send(null);
}

request関数はXMLHttpRequestを投げ、その結果に応じてAutoPatchWork.loadか、AutoPatchWork.errorイベントを投げます。
このように何らかの処理→イベント発行→それを受け取って次の処理を行い、またイベントを投げるという繰り返しをしています。

さらに、DOM要素の追加には、カスタムMutationEventを使用しています。

var docs = get_next_elements(htmlDoc);
docs.forEach(function(doc,i,docs){
	var insert_node = options.append_point.insertBefore(document.importNode(doc, true), options.insert_point);
	var mutation = {
		targetNode:insert_node,
		type:'AutoPatchWork.DOMNodeInserted',
		canBubble:true,
		cancelable:false,
		relatedNode:options.append_point,
		prevValue:null,
		newValue:url,
		attrName:null,
		attrChange:null,
	};
	dispatch_mutation_event(mutation);
	docs[i] = insert_node;
});

function dispatch_mutation_event(opt){
	var ev = document.createEvent('MutationEvent');
	with (opt) {
		ev.initMutationEvent(type, canBubble, cancelable, relatedNode, prevValue, newValue, attrName, attrChange);
		targetNode.dispatchEvent(ev);
	}
}

MutationEventはDOMの変更を通知するためのイベントで、 DOMNodeInserted などが該当します。AutoPatchWorkで追加した要素をAutoPatchWork.DOMNodeInsertedイベントとして発行しているので、このイベントを監視すればAutoPagerizeでいうaddFilterのような処理ができます。

window.addEventListener('AutoPatchWork.DOMNodeInserted', target_rewrite, false);
function target_rewrite(evt){
	if (evt && evt.target){
		var as = evt.target.getElementsByTagName('a');
		for (var i = 0, l = as.length;i < l;i++){
			var a = as[i], _a = a.getAttribute('href');
			if (_a && !/^javascript:/.test(_a) && !/^#/.test(_a))
				a.setAttribute('target',options.TARGET_WINDOW_NAME);
		}
	}
}

ターゲットの書き換えはこんな感じです。

また、word highlightの場合、AutoPagerize用のコードはこう書いていますが、

if (window.AutoPagerize) {
	boot();
} else {
	window.addEventListener('GM_AutoPagerizeLoaded',boot,false);
}
function boot(){
	window.AutoPagerize.addFilter(function(docs) {
		docs.forEach(function(doc){
			highlight(doc);
			if (TARGET_BLANK) target_google(doc,++page);
		});
		word_lists.forEach(function(item){
			item.label.textContent = item.word + '(' + item.xpath.get({result_type:XPathResult.NUMBER_TYPE}).numberValue + ')';
		});
		layers = xp_all.get();
	});
}

AutoPatchWorkのイベントベースではこうなります。

window.addEventListener('AutoPatchWork.DOMNodeInserted',function(e){
	highlight(e.target);
	if (TARGET_BLANK) target_google(e.target, ++page);
},false);
window.addEventListener('AutoPatchWork.pageload',function(e){
	word_lists.forEach(function(item){
		item.label.textContent = item.word + '(' + item.xpath.get({result_type:XPathResult.NUMBER_TYPE}).numberValue + ')';
	});
	layers = xp_all.get();
},false);

window.AutoPagerize.addFilterはaddFilterが定義されるタイミングを待つ必要がありますが、AutoPatchWork.DOMNodeInsertedではタイミングを気にする必要がありません。

まとめ

AutoPatchWorkが発行するイベントはウェブサイト側のスクリプトからも捕まえることができます。そのためイベントベースで書いておくと、ユーザーサイドスクリプトの連携がやり易くなるだけでなく、ウェブサイト側のスクリプトとも連携を取ることができるようになります(stopPropagationでAutoPatchWorkを止めることもできるので、その辺りの良し悪しも議論の余地があるところ)。
イベントベースが流行ったらScript同士の連携が面白くなりそうなので、ちょっと期待しつつ。

そのほか

少なくともChromeのextensionが正式にリリースされるまでは AutoPatchWork で実験的な試みを続けようと思っているので、AutoPatchWorkの実装は今回書いたものから大きく変わるかもしれません。また、セキュリティ的に問題があるかもしれません。その辺りはご了承ください。

ちなみに、この場合のセキュリティリスクとして、拡張コンテキストから、クロスドメインなリクエストを投げられてしまうケースが考えられます(実際にできるかは不明です)が、AutoPatchWorkではss-o.netドメインへのリクエストだけしか許可していないので、これによるリスクはまずないだろうと考えられます。それ以外にはAutoPatchWorkが重要なデータを持つこともないので、この拡張自体のセキュリティリスクは高くないと考えています。

*1:IEのDOM Eventサポートが独自仕様過ぎるので、GreasemonkeyやUser Scriptsでしか使いにくいのが残念です