JavaScript

WebWorkerでXMLHttpRequest

More than 5 years have passed since last update.


XMLHttpRequestをWebWorkerで実行する

Senchaが公開したHTML5のFacebookクライアント、Fastbookの高速化手法として、

XMLHttpRequestをWebWorkerで実行するのがあるそうです。

ということで、実際にやってみました。


WebWorkerの呼び出し

jQueryを用いた環境で使いやすいように、jQueryの$.ajaxインターフェイスに似せています。

ただし、XMLHttpRequestをWebWorkerからは取得できないので、全く一緒ではありません。


ExecutorServiceの実装

WebWorkerを大量に作成すると負荷がかかるので、

少し手間ですが、同時実行数を制限し、リクエストをキューイングする

ExecutorServiceを実装します。


jquery.ajax.js

  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です。


jquery.ajax.js

  var AjaxService = new ExecutorServece('/js/workers/xhr.js');



WebWorkerの実行

WebWorkerの実行は、ExecutorServiceインスタンスのexecuteメソッドを通して行います。

executeメソッドの第一引数messageは、WebWorkerに送られるオブジェクトです。

このオブジェクトにXMLHttpRequestが送信するURLやデータ、HTTPHeaderなどを指定します。

executeメソッドの第二引数には、成功やエラーなどのハンドラを定義します。

また、ハンドラが実行されるときのthisキーワードを指定できるcontextもこちらに指定できます。

最後に、jQuery環境で扱いやすいように、jQuery.ajaxのラッパーを作成しています。


jquery.ajax.js

  (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に応じて切り替えます。


workers/xhr.js

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オブジェクトを作成しています。


test.js

    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を非同期でバックグラウンドスレッドで実行することで、通信の高速化を実現しているので、それと同様なのでしょう。