LoginSignup
42
38

More than 5 years have passed since last update.

WebWorkerでXMLHttpRequest

Posted at

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

42
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
38