Скрещиваем WebWorker и Promise

    Если уж есть необходимость скрещивать WebWorker с XMLHttpRequest, то пора бы скрестить его с любой функцией, а заодно разобраться с обещаниями ES6.

    Цель — научиться делать вот так:
    new PromiseWorker(array => array.sort()).Invoke([3,2,1]).then(result => console.log(result));
    (Здесь и далее используются arrow functions для краткости)



    Как известно, true-way для создания воркера — передать путь к файлу в качестве единственного параметра: new Worker("/JS/worker.js") . повесить обработчик onmessage, вызывать postmessage и придерживаться подобного стиля в файле воркера. По моему мнению на одну функцию слишком жирно создавать целый файл, да и возиться с событиями-обработчиками уже не комильфо. Хорошо, что есть Blob и уже упомянутые Promises.

    Сначала надо сделать преобразование входной функции в приемлемый для воркера вид:
    var FnToWorker = fn => {
       var workerBody = "self.addEventListener('message'," +
            "function (d) {" +
                "var result;" +
                "try {" +
                    "result = (" + fn.toString() + ")(d.data.Data);" +
                    "self.postMessage({ Result: result, Id: d.data.Id });" +
                "} catch (e) {" +
                    "self.postMessage({ Error: e, Id: d.data.Id });" +
                "}" +
            "});"
        var worker = new Worker(URL.createObjectURL(new Blob([workerBody])));
        return worker;
    }
    

    Да, тут отвратительная конкатенация строк, fn.toString() и прочие ужасные вещи… главное что этот код можно один раз написать и забыть о нем.
    Как указали в комментариях, данная конструкция накладывает некоторые ограничения: функция должна не иметь внешних зависимостей (переменные замыкания, неразрешенные для воркеров объекты window), т.к. к ним не будет доступа из потока воркера. Функции, созданные с помощью системных функций (например, Function.prototype.bind), не могут быть использованы в воркерах из-за того, что fn.toString() не вернет тело функции.

    Вот так будет выглядеть Invoke:
    var promises = []; //Очередь вызовов
    var Invoke = data => {
        var message = { Data: data, Id: performance.now() }; //У каждого сообщения Id, чтобы ничего не путалось
        var p = new Promise((resolve, reject) => {
            promises[message.Id] = { resolve: resolve, reject: reject };
        });
        worker.postMessage(message); //Запускаем воркер
        return p;
    }
    

    Использовать Promise не сложно: в конструктор передаем функцию от двух аргументов: resolve и reject. Это функции которые надо будет вызвать в случае успеха операции и неуспеха соответственно. В нашем случае они будут вызываться после того, как отработает воркер:
    var OnMessage = data => {
        if (data.data.Error) {
            promises[data.data.Id].reject(data.data.Result);
        } else {
            promises[data.data.Id].resolve(data.data.Result);
        }
        promises[data.data.Id] = undefined;
    }
    


    Ну и вот так оно будет выглядеть в сборе: http://jsfiddle.net/sXJ4M/1/

    Конечно, сложно себе представить, где можно использовать воркеры в интернет-магазине, но в достаточно больших и сложных приложениях они очень помогают.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      +1
      "result = (" + fn.toString() + ")(d.data.Data);"


      Как-то это неправильно. Не то, чтобы неэстетично, а вообще.

      Держу пари, что если передать в качестве fn функцию, которая использует переменные из замыкания, на выходе получим трудноловимый баг.

      А если таким аргументом будет любая функция, toString которой возращает "function () { [native code] }", например, результат Function.prototype.bind() в том же Хроме, результатом будет ошибка синтаксиса.
        0
        Переменные из замыкания нельзя и с этим ничего не сделать, воркер к ним не доступится. У воркеров есть довольно большие ограничения.

        Насчет bind спасибо, не знал, добавил в пост.
        0
        Promise судя по caniuse.com/#search=promise совсем плохо поддерживается браузерами кроме новых десктопных FF, Chrome и Opera.
          +1
          пользуетесь ES6 — пользуйтесь и интерполяцией строк :)
            0
            А есть где-нибудь толковая статья, посвященная этой теме? А то так и не могу понять, что и как будет реализовано в ES6 и что уже включено в Firefox.
              0
              я не знаю, честно говоря, я traceur пользуюсь.

          Only users with full accounts can post comments. Log in, please.