#XMLHttpRequestをWebWorkerで実行する
Senchaが公開したHTML5のFacebookクライアント、Fastbookの高速化手法として、
XMLHttpRequestをWebWorkerで実行するのがあるそうです。
ということで、実際にやってみました。
##WebWorkerの呼び出し
jQueryを用いた環境で使いやすいように、jQueryの$.ajaxインターフェイスに似せています。
ただし、XMLHttpRequestをWebWorkerからは取得できないので、全く一緒ではありません。
###ExecutorServiceの実装
WebWorkerを大量に作成すると負荷がかかるので、
少し手間ですが、同時実行数を制限し、リクエストをキューイングする
ExecutorServiceを実装します。
var ExecutorServece = (function() {
ExecutorServece.prototype.maxThreads = 5;
function ExecutorServece(path, maxThreads) {
this.path = path;
this._queue = [];
this._pool = [];
this._running = 0;
if (maxThreads) this.maxThreads = maxThreads;
}
ExecutorServece.prototype.execute = function(message, options) {
if (this._running >= this.maxThreads) {
return this._queue.push(arguments);
} else {
return this._execute.apply(this, arguments);
}
};
ExecutorServece.prototype._execute = function(message, options) {
var context, onComplete, onError, onMessage, worker,
_this = this;
++this._running;
worker = this._pool.shift() || new Worker(this.path);
context = options.context || this;
onMessage = function(ev) {
if (options.success) options.success.apply(context, ev.data);
return onComplete(ev);
};
onError = function(ev) {
if (options.error) options.error.apply(context, ev.data);
return onComplete(ev);
};
onComplete = function(ev) {
if (options.complete) options.complete.apply(context, ev.data);
_this._release(worker, onMessage, onError);
return _this._dequeue();
};
worker.addEventListener('message', onMessage, false);
worker.addEventListener('error', onError, false);
return worker.postMessage(message);
};
ExecutorServece.prototype._release = function(worker, onMessage, onError) {
worker.removeEventListener('message', onMessage, false);
worker.removeEventListener('error', onError, false);
this._pool.push(worker);
return --this._running;
};
ExecutorServece.prototype._dequeue = function() {
var args;
args = this._queue.shift();
if (args) return this._execute.apply(this, args);
};
return ExecutorServece;
})();
###ExecutorServiceインスタンスの作成
ExecutorServiceの第一引数はWebWorkerのJSのパス、第二引数は同時接続数(任意)です。
なお、同時接続数のデフォルト値は5です。
var AjaxService = new ExecutorServece('/js/workers/xhr.js');
###WebWorkerの実行
WebWorkerの実行は、ExecutorServiceインスタンスのexecuteメソッドを通して行います。
executeメソッドの第一引数messageは、WebWorkerに送られるオブジェクトです。
このオブジェクトにXMLHttpRequestが送信するURLやデータ、HTTPHeaderなどを指定します。
executeメソッドの第二引数には、成功やエラーなどのハンドラを定義します。
また、ハンドラが実行されるときのthisキーワードを指定できるcontextもこちらに指定できます。
最後に、jQuery環境で扱いやすいように、jQuery.ajaxのラッパーを作成しています。
(function($) {
option_keys = ['type', 'url', 'data', 'dataType', 'contentType', 'headers'];
defaults = {};
$.ajaxSetup = function(options) {
var key, value, _results;
_results = [];
for (key in options) {
value = options[key];
_results.push(defaults[key] = value);
}
return _results;
};
$.ajax = function(options) {
var beforeSend, key, message, success, _i, _len, _success;
beforeSend = options.beforeSend || defaults.beforeSend;
if (beforeSend && beforeSend === false) return;
message = {};
for (_i = 0, _len = option_keys.length; _i < _len; _i++) {
key = option_keys[_i];
message[key] = options[key] || defaults[key];
}
_success = options.success || defaults.success;
if (options.dataFilter) {
success = function() {
var context;
context = options.context || this;
arguments[0] = options.dataFilter.apply(context, arguments);
return _success.apply(context, arguments);
};
} else {
success = _success;
}
AjaxService.execute(message, {
success: success,
error: options.error || defaults.error,
complete: options.complete || defaults.complete,
context: options.context
});
};
})(jQuery);
##WebWorkerの実装
WebWorker側では、呼び出し元のオブジェクトを元にXMLHttpReqeustを作成し、送信します。
あらかじめ、返却するデータの種類毎にハンドラを作成しておき、
呼び出し元のオブジェクトのdataTypeに応じて切り替えます。
var onLoad = function (callback) {
return function onLoad() {
if (this.readyState === 4) {
if (this.status === 200) {
callback.call(this, this);
} else {
var msg = 'error';
throw [this, msg];
}
}
};
},
postImage = onLoad(function(xhr) {
var blob = new Blob([xhr.response]),
reader = new FileReader();
reader.onload = function(ev) {
var data = ev.target.result;
data = data.replace('data:;', 'data:'+ xhr.getResponseHeader('Content-Type') + ';');
postMessage([data, 'image']);
};
reader.readAsDataURL(blob);
}),
postXML = onLoad(function(xhr) {
postMessage(xhr.responseXML, 'xml');
}),
postJSON = onLoad(function(xhr) {
postMessage(JSON.parse(xhr.responseText), 'json');
}),
postText = onLoad(function(xhr) {
postMessage(xhr.responseText, 'text');
});
addEventListener('message', function(ev) {
var options = ev.data,
xhr = new XMLHttpRequest();
xhr.open (options.type || "GET", options.url);
if (options.contentType) {
if (!options.headers) options.headers = {};
options.headers['Content-Type'] = options.contentType;
}
if (options.headers) {
for (var name in options.headers) {
xhr.setRequestHeader(name, options.headers[name]);
}
}
switch (options.dataType && options.dataType.toLowerCase()) {
case 'binary':
case 'image':
xhr.responseType = "blob";
xhr.onreadystatechange = postImage;
break;
case 'json':
xhr.onreadystatechange = postJSON;
break;
case 'xml':
xhr.onreadystatechange = postXML;
break;
default:
xhr.onreadystatechange = postText;
}
xhr.send (options.data);
}, false);
##リクエストの送信
リクエストの送信はjQuery.ajaxメソッドとほぼ同じです。
※一部対応していないオプションがあります。
以下のサンプルでは、jQuery.ajaxでサポートしていない画像をbase64エンコードされたデータを取得して、Imageオブジェクトを作成しています。
function TestLoadImageWebWorker() {
var _this = this;
this.start = new Date().getTime();
this.log = function() {
return _this.constructor.prototype.log.apply(_this, arguments);
};
}
TestLoadImageWebWorker.prototype.log = function() {
var end;
end = new Date().getTime();
return console.log('TestLoadImage: ' + (end - this.start));
};
TestLoadImageWebWorker.prototype.load = function(url) {
return $.ajax({
url: url,
dataType: 'image',
contentType: 'image/jpeg',
success: this._dataToImage,
context: this
});
};
TestLoadImageWebWorker.prototype._dataToImage = function(base46Data) {
this.image = new Image;
this.image.onload = this.log;
return this.image.src = base46Data;
};
$(function() {
var i, runner, _results;
runner = new TestLoadImageWebWorker;
for (i = 1; i <= 1000; i++) {
runner.load('/img/88-1.jpg?' + i));
}
});
実行速度は通常の画像読み込みとはさほど差はありません。
ただし、別スレッドで実行されるのでメインスレッドに負荷をかけません。
リクエストを送信しても、UIを操作させる場合に有用です。
iPhoneのネイティブアプリでもNSURLConnectionを非同期でバックグラウンドスレッドで実行することで、通信の高速化を実現しているので、それと同様なのでしょう。