Главная » Статьи » Простое введение в Web Workers в JavaScript

Простое введение в Web Workers в JavaScript

Простое введение в Web Workers в JavaScript

От автора: какое-то время тому назад , когда JavaScript был только представлен, никто слишком не волновался о производительности. JavaScript был разработан, чтобы быть простым языком для запуска небольших фрагментов скрипта на веб-странице. Это был оборок — упрощенный язык скриптов для программистов-любителей. Он определенно не предназначался для того, чтобы управлять чьим-либо бизнесом.

Перенесемся почти на 25 лет вперед — теперь JavaScript овладел Сетью. У него есть профессиональные обязанности и, наконец, профессиональные качества. Одним из примеров этого являются Web Workers.

Web Workers предназначены для того, чтобы вы могли выполнять громоздкие задачи, не замораживая страницу. Например, представьте, что вы хотите выполнять сложные вычисления, когда кто-то нажимает кнопку. Если вы начнете выполнять эту задачу сразу же, вы все заблокируете. Человек, использующий страницу, не сможет прокрутить ее вниз или кликнуть что-либо. Они могут даже получить страшное сообщение об ошибке «эта страница не отвечает».

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

(Понимание того, как работают потоки, немного выходит за рамки этой статьи. Но основная идея заключается в том, что современные операционные системы запускают сотни потоков одновременно и большую часть времени быстро переключаются с одного потока на другой. Фактически, они переключаются так быстро и плавно, что кажется, что все происходит одновременно.)

Web Workers позволяют выполнять любую трудоемкую работу в фоновом режиме. Процесс прост:

Вы создаете web worker.

Вы говорите web worker, что делать. (Например, заняться этими числами!)

Вы запускаете web worker.

Когда web worker готов, он говорит вам об этом, и оттуда приходит код. (Например, показать результаты на странице.)

Давайте рассмотрим все подробнее!

Создание трудоемкой задачи

Прежде чем вы сможете увидеть преимущества web worker, вам нужно написать действительно медленный код. Нет смысла использовать web worker для быстрых задач, потому что они не блокируют страницу.

Рассмотрим, например, поиск простых чисел, показанный ниже. (Он размещен на CodePen, так что вы можете попробовать его, посмотреть код и даже внести изменения.)

В этом примере используются простые числа, попадающие в заданный диапазон. Вы выбираете диапазон, используя два текстовых поля на странице. Выберите относительно узкий диапазон (скажем, от 1 до 100 000), и задача будет выполнена за считанные секунды, не доставляя никому неудобств. Но запустите более широкий поиск (скажем, от 1 до 1 000 000), и ваша страница может перестать отвечать на секунды или минуты. Вы не сможете ничего кликать, прокручивать или взаимодействовать.

Очевидно, что эту страницу можно улучшить с помощью web worker. Но прежде чем вы доберетесь до этого, вам нужно внимательнее рассмотреть текущий код JavaScript. Прямо сейчас, когда вы нажимаете кнопку «Поиск», запускается функция doSearch(), показанная здесь:

function doSearch() { // Получаем два числа в текстовых полях. Это диапазон поиска. var fromNumber = document.getElementById("from").value; var toNumber = document.getElementById("to").value; // Выполняем поиск простых чисел. (Это время-затратный шаг.) var primes = findPrimes(fromNumber, toNumber); // Принимаем результат (массив простых чисел), перебираем его через цикл, // и вставляем все простые числа в один длинный кусок текста. var primeList = ""; for (var i=0; i<primes.length; i++) { primeList += primes[i]; if (i != primes.length-1) primeList += ", "; } // Отображаем список простых чисел на странице. var displayList = document.getElementById("primeContainer"); displayList.textContent = primeList; // Обновляем текст статуса, чтобы сообщить пользователю, что сейчас произошло. var statusDisplay = document.getElementById("status"); if (primeList.length == 0) { statusDisplay.textContent = "Search didn't find any results."; } else { statusDisplay.textContent = "The results are here!"; }
}

Этот код совершенно непримечательный. Он делает то, что делает почти каждый базовый JavaScript — сначала он получает некоторую информацию со страницы, затем выполняет вычисления, а затем вставляет результаты обратно в div, чтобы вы могли их увидеть.

Код, который фактически выполняет поиск простых чисел, находится в другой функции с именем findPrimes(). Вам не нужно понимать, как работает поиск простых чисел, чтобы использовать этот пример. Мы просто используем его, потому что это задача, которая проста для кодирования, но сложна в отношении вычислений, а это означает, что это может занять некоторое значительное время. Если вам интересно посмотреть математику, которая заставляет эту страницу работать, посмотрите в примере CodePen функцию findPrimes().

Выполняем задачу в фоновом режиме

Функция web worker связана с объектом под названием Worker. Когда вы хотите запустить что-то в фоновом режиме, вы создаете новый Worker, передаете ему некоторый код и отправляете некоторые данные.

Вот пример, который создает нового web worker, который запускает код в файле с именем PrimeWorker.js:

var worker = new Worker("PrimeWorker.js");

Код, который выполняет web worker, всегда хранится в отдельном файле JavaScript. Этим шаблоном начинающим программистам не рекомендуется писать код web worker, который пытается использовать глобальные переменные или напрямую обращаться к элементам на странице. Ни одна из этих операций невозможна. Почему? Потому что могут возникнуть проблемы, если несколько потоков пытаются манипулировать одними и теми же данными одновременно. Это означает, что у кода в PrimeWorker.js нет способа записать простые числа в элемент div. Вместо этого код web worker должен отправить свои данные обратно в код JavaScript на странице, и позволить коду веб-страницы отобразить результаты.

Веб-страницы и web worker общаются путем обмена сообщениями. Чтобы отправить данные worker, мы вызываем метод worker postMessage():

worker.postMessage(myData);

Затем worker получает событие onMessage, которое предоставляет копию данных. Точно так же, когда worker необходимо вернуться к веб-странице, он вызывает собственный метод postMessage() вместе с некоторыми данными, и веб-страница получает событие onMessage.

Перед тем, как рассмотреть это подробнее, нам нужно разобрать еще одну проблему. Функция postMessage() допускает только одно значение. Этот факт является камнем преткновения для поиска простых чисел, потому что ему нужны две части данных (два числа диапазона). Решение состоит в том, чтобы упаковать эти две части в литерал объекта. Этот код демонстрирует пример, в котором мы передаем объекту два свойства (первое с именем from и второе с именем to) и присваивает значения им обоим:

worker.postMessage( { from: 1, to: 20000 }
);

Помня об этом, вы можете пересмотреть функцию doSearch(), о которой шла речь ранее. Вместо того, чтобы выполнять поиск простого числа, функция doSearch() создает worker и заставляет его выполнять фактическую задачу.

var worker; function doSearch() { // Disable the button, so the user can't start more than one search // at the same time. searchButton.disabled = true; // Create the worker. worker = new Worker("PrimeWorker.js"); // Hook up to the onMessage event, so you can receive messages // from the worker. worker.onmessage = receivedWorkerMessage; // Get the number range, and send it to the web worker. var fromNumber = document.getElementById("from").value; var toNumber = document.getElementById("to").value; worker.postMessage( { from: fromNumber, to: toNumber } ); // Let the user know that things are on their way. statusDisplay.innerHTML = "A web worker is on the job ("+ fromNumber + " to " + toNumber + ") ...";
}

Теперь в действие вступает код в файле PrimeWorker.js. Он получает событие onMessage, выполняет поиск, а затем отправляет новое сообщение обратно на страницу со списком простых чисел.

onmessage = function(event) { // The object that the web page sent is stored in the event.data property. var fromNumber = event.data.from; var toNumber = event.data.to; // Using that number range, perform the prime number search. var primes = findPrimes(fromNumber, toNumber); // Now the search is finished. Send back the results. postMessage(primes);
}; function findPrimes(fromNumber, toNumber) { // (The boring prime number calculations go in this function.)
}

Когда worker вызывает postMessage(), он запускает событие onMessage, которое вызывает эту функцию на веб-странице:

function receivedWorkerMessage(event) { // Получаем список простых чисел. var primes = event.data; // Копируем список на страницу. ... // Предоставляем возможность выполнять следующий поиск. searchButton.disabled = false;
}

В целом структура кода немного изменилась, но логика в основном та же. Результат, однако, резко отличается. Теперь, когда выполняется поиск больших простых чисел, страница остается интерактивной. Вы можете прокрутить страницу вниз, ввести текст и выбрать числа в списке из предыдущего поиска. Посмотрите этот CodePen:

Обработка ошибок worker

Метод postMessage() является ключевым аспектом взаимодействия с web worker. Однако есть еще один способ, с помощью которого web worker может уведомить веб-страницу — событие onerror, которое сигнализирует об ошибке:

worker.onerror = workerError;

Теперь, если какой-то хитрый сценарий или неверные данные вызывают ошибку в фоновом коде, подробности ошибки упаковываются и отправляются обратно на страницу. Вот некоторый код веб-страницы, который просто отображает текст сообщения об ошибке:

function workerError(error) { statusDisplay.textContent = error.message;
}

Наряду со свойством message, объект ошибки также включает в себя свойства lineno и filename, которые сообщают номер строки и имя файла, в котором произошла ошибка.

Отмена фоновой задачи

Теперь, когда мы создали базовый пример web worker, можно добавить к нему некоторые улучшения. Прежде всего, это поддержка отмены, которая позволяет странице закрыть worker, пока он еще работает.

Есть два способа остановить worker. Во-первых, worker может быть остановлен через вызов close(). Но чаще всего страница, создавшая worker, закрывает его, вызывая метод worker terminate(). Например, вот код, который вы можете использовать для включения простой кнопки отмены:

function cancelSearch() { worker.terminate(); statusDisplay.textContent = ""; searchButton.disabled = false;
}

Нажмите эту кнопку, чтобы остановить текущий поиск и снова включить кнопку поиска. Просто помните, что после того, как worker остановлен таким образом, вы больше не можете отправлять сообщения, и его нельзя использовать для выполнения каких-либо дополнительных операций. Чтобы выполнить новый поиск, вам нужно создать новый объект worker.

Передача более сложных сообщений

Последний прием, который мы изучим — возвращение информации о прогрессе. Это более продвинутый прием, потому что вам нужно заставить web worker продолжать общаться с веб-страницей. Тем не менее, это полезный навык, который стоит освоить, потому что вы будете использовать этот тип общения в более сложных примерах для web worker.

Как вы уже узнали, у web worker есть только один способ общения с веб-страницей — метод postMessage(). Таким образом, чтобы создать этот пример, web worker необходимо отправить два типа сообщений: уведомления о прогрессе работы (пока задача выполняется) и список простых чисел (когда задача закончена). Суть в том, что разница между этими двумя сообщениями должна быть понятна, и тогда обработчик событий onMessage на странице может определить разницу между двумя типами сообщений.

Лучший подход — добавить к сообщению немного дополнительной информации. Например, когда web worker отправляет информацию о прогрессе, он может добавить в сообщение текстовую метку «Progress». И когда web worker отправляет список простых чисел, он может добавить метку «PrimeList».

Чтобы объединить всю необходимую информацию в одно сообщение, вам нужно создать литерал объекта. Это делается с помощью той же технологии, что и создание веб-страницы, используемой для отправки данных диапазона чисел. Дополнительная часть информации — это текст, описывающий тип сообщения, который помещается в свойство, называемое в этом примере messageType. Фактические данные передаются во втором свойстве с именем data.

Вот как можно переписать код web worker, чтобы добавить тип сообщения в список простых чисел:

onmessage = function(event) { // Выполняем поиск простых чисел. var primes = findPrimes(event.data.from, event.data.to); // Отправляем обратно результаты. postMessage( {messageType: "PrimeList", data: primes} );
};

Код в функции findPrimes() также использует метод postMessage() для отправки сообщения обратно на веб-страницу. Он использует те же два свойства — messageType и data. Но теперь messageType указывает, что сообщение является уведомлением о прогрессе, а данные содержат процент выполнения:

function findPrimes(fromNumber, toNumber) { ... // Рассчитываем процент выполнения. var progress = Math.round(i/list.length*100); // Отправляем обновление прогресса только в том случае, если прогресс изменился // как минимум на 1%. if (progress != previousProgress) { postMessage( {messageType: "Progress", data: progress} ); previousProgress = progress; } ...
}

Когда страница получает сообщение, сначала должна выполняться проверка свойства messageType, чтобы определить, какой тип сообщения страница только что получила. Если это простой список, то результаты отображаются на странице. Если это уведомление о прогрессе, то обновляется текст прогресса:

function receivedWorkerMessage(event) { var message = event.data; if (message.messageType == "PrimeList") { var primes = message.data;// Выводим список простых чисел. Этот код тот же, что и раньше. ... } else if (message.messageType == "Progress") { // Отображаем текущий прогресс. statusDisplay.textContent = message.data + "% done …"; }
}

Этот вид передачи сообщений может быть сложным. Но дополнительная работа того стоит, потому что она обеспечивает безопасность кода. Поток веб-страницы и поток web worker никоим образом не могут пересечься — что не гарантируется, если вы используете потоки в других языках программирования.

Функция прогресса уже есть в предыдущем примере CodePen. Чтобы увидеть весь код, посмотрите CodePen.

Заключение

Сейчас, поиск простых чисел использует web worker самым простым способом — выполнить одну четко определенную задачу. Ваши страницы могут не быть такими простыми. Вот несколько примеров того, как вы можете продвинуться дальше:

Создать несколько web worker. Ваша страница не должна привязываться к одному worker. Например, представьте, что вы хотите, чтобы посетитель запускал несколько поисков простых чисел одновременно. Вы можете создать нового web worker для каждого поиска и отслеживать всех в массиве.

Создать web worker внутри web worker. web worker может запускать собственных web worker, отправлять им сообщения и получать их обратно. Этот метод полезен для сложных вычислительных задач, требующих рекурсии, таких как вычисление последовательности Фибоначчи.

Периодически выполнять задачи с помощью web worker. web worker могут использовать функции setTimeout() и setInterval(), так же , как обычные веб-страницы. Например, вы можете создать web worker, который каждую минуту проверяет наличие новых данных на веб-сайте.

Web worker — это один из наиболее важных методов, с помощью которых JavaScript сократил разрыв между миром браузеров и миром настольных приложений. Они существуют всего несколько лет, но они уже необходимы для серьезной работы с JavaScript.

Автор: Matthew MacDonald

Источник: https://medium.com

Редакция: Команда webformyself.