WebWorkerでXMLHttpRequest

#XMLHttpRequestをWebWorkerで実行する
Senchaが公開したHTML5のFacebookクライアント、Fastbookの高速化手法として、
XMLHttpRequestをWebWorkerで実行するのがあるそうです。
ということで、実際にやってみました。

##WebWorkerの呼び出し
jQueryを用いた環境で使いやすいように、jQueryの$.ajaxインターフェイスに似せています。
ただし、XMLHttpRequestをWebWorkerからは取得できないので、全く一緒ではありません。

###ExecutorServiceの実装
WebWorkerを大量に作成すると負荷がかかるので、
少し手間ですが、同時実行数を制限し、リクエストをキューイングする
ExecutorServiceを実装します。

```js: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です。

```js: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のラッパーを作成しています。

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

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

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