Статьи

JavaScript Web Workers: руководство для начинающих

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

Когда JavaScript был задуман в первые дни Интернета, направление развития Интернета было неясно. Из-за постоянных, быстрых изменений в отрасли и экосистеме, необходимости обратной совместимости браузеров и веб-стандартов развитие JavaScript стало постоянным потоком исправлений, взломов и последствий.

JavaScript веб-работники загружают

Современные мобильные устройства обычно поставляются с 8+ ядрами ЦП или 12+ ядрами GPU. Настольные и серверные процессоры имеют до 16 ядер, 32 потока или более.

В этой среде узкое место имеет наличие доминирующей однопоточной среды программирования или сценариев.

JavaScript является однопоточным

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

Это ужасно неэффективно, особенно по сравнению с другими языками .

Если мы перейдем к JS Bin и запустим этот код в консоли JavaScript браузера:

//noprotect i = 0; while (i < 60000) { console.log("The number is " + i); i++; } 

… Весь веб- сайт jsbin.com перестанет отвечать на запросы до тех пор, пока браузер не посчитает и не зарегистрирует до 60 000.

Мы не сможем взаимодействовать с чем-либо на странице, потому что браузер занят.

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

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

Веб-работники

W3C опубликовал первый проект стандарта веб-работников в 2009 году. С полной спецификацией можно ознакомиться на веб-сайте рабочей группы по технологиям веб-гипертекстовых приложений — или WHATWG — альтернативной W3C организации по веб-стандартам.

Веб-работники — это асинхронная система или протокол, позволяющий веб-страницам выполнять задачи в фоновом режиме независимо от основного потока и пользовательского интерфейса веб-сайта. Это изолированная среда, которая изолирована от объекта window объекта document , прямого доступа в Интернет и лучше всего подходит для длительных или требовательных вычислительных задач.

Помимо веб-работников — системы, предназначенной для многопоточности, — существуют и другие способы выполнения асинхронной обработки в JavaScript, такие как асинхронные вызовы Ajax и цикл обработки событий.

Чтобы продемонстрировать это, мы вернемся к JS Bin и попробуем этот фрагмент:

 console.log("A"); setTimeout(function(){console.log("B");},2000); console.log("C"); setTimeout(function(){console.log("D");},0); console.log("E"); setTimeout(function(){console.log("F");},1000); 

Когда мы запускаем это, наша логарифмическая последовательность A, C, E, D, F, B Браузер сначала планирует операции без тайм-аута по мере их поступления, а затем выполняет функции setTimeout() в порядке их соответствующих указанных задержек. Однако эта асинхронность не должна автоматически сопоставляться с многопоточностью. В зависимости от хост-машины это часто может быть просто однопотоковый стек вызовов в порядке, который мы объяснили.

Веб-работники и многопоточность

Как объясняет справочный веб-сайт JavaScript в Mozilla , веб-работники являются «средством для веб-контента запускать скрипты в фоновых потоках»

Мы используем их следующим образом : мы проверяем наличие конструктора Worker() в браузере, и, если он доступен, мы создаем экземпляр рабочего объекта с URL-адресом сценария в качестве аргумента. Этот скрипт будет выполнен в отдельном потоке.

Сценарий должен обслуживаться с того же хоста или домена из соображений безопасности, и это также является причиной того, что веб-работники не будут работать, если мы откроем файл локально по схеме file:// .

 if (typeof(Worker) !== "undefined") { worker = new Worker("worker.js"); } 

Теперь мы определим этот код в файле worker.js :

 i = 0; while (i < 200000) { postMessage("Web Worker Counter: " + i); i++; } 

Если вы хотите писать высококачественные файлы JavaScript для веб-рабочих, ознакомьтесь с нашей книгой « JavaScript: лучшие практики» .

Разделение потоков

Здесь важно отметить разделение области выполнения window и document в потоке основного окна браузера и worker области.

Чтобы использовать worker поток, эти две области должны иметь возможность обмениваться данными. Для этого мы используем функцию postMessage() в файле worker.js — для отправки сообщений в основной поток браузера — и прослушиватель worker.onmessage в главном потоке для прослушивания worker сообщений.

Мы также можем отправлять сообщения из основного потока браузера в worker поток или функцию. Единственное отличие состоит в том, что мы обращаем вещи и вызываем worker.postMessage() в главном потоке и onmessage в рабочем потоке. Цитирую ссылку разработчика Mozilla:

Обратите внимание, что onmessage и postMessage() должны быть подвешены к объекту Worker при использовании в основном потоке сценария, но не при использовании в работнике. Это связано с тем, что внутри работника работник фактически является глобальной областью действия.

Мы можем использовать метод terminate() таким же образом, чтобы завершить выполнение нашего работника.

Имея все это в виду, мы подходим к этому примеру:

index.html

 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>Web Workers Example</title> <style type="text/css"> body {padding-top:28px;} .output-cont {margin-left:12%; margin-top:28px;} .output-cont h3 {width:200px; height:100%;} .output-cont button {padding:4px 8px; font-size:1.1rem; font-family:sans-serif; } </style> </head> <body> <div class="output-cont"><button onclick="testWorker()">start worker</button><h3 id="workerOutput"></h3><button onclick="terminateWorker()">terminate worker</button></div> <br/> <div class="output-cont"><button onclick="testMainThread()">start blocking thread</button><h3 id="mainThreadOutput"></h3></div> <br/> <div class="output-cont"><button onclick="alert('browser responsive!')">test browser responsiveness</button></div> <script> var worker; function testWorker() { if (typeof(Worker) !== "undefined") { if (typeof(worker) == "undefined") { worker = new Worker("worker.js"); } worker.onmessage = function(event) { document.getElementById("workerOutput").innerHTML = event.data; }; } else { document.getElementById("workerOutput").innerHTML = "Web Workers are not supported in your browser"; } } function terminateWorker() { worker.terminate(); worker = undefined; } function testMainThread() { for (var i = 0; i < 200000; i++) { document.getElementById("mainThreadOutput").innerHTML = "Main Thread Counter: " + i; } } </script> </body> </html> 

и worker.js :

 i = 0; while (i < 200000) { postMessage("Web Worker Counter: " + i); i++; } 

Это дает нам возможность проверить влияние выполнения основного потока на поведение и производительность страницы по сравнению с эффектами веб-работника.

В этом уроке мы использовали http-server для локального обслуживания файлов.

Тестирование веб-работников JavaScript

Теперь мы можем видеть, что рабочий поток не блокирует интерактивность основного процесса браузера и циклический просмотр 200 000 номеров не влияет на основной поток. Числа в элементе #workerOutput обновляются на каждой итерации.

Блокирующий поток, или основной поток, когда задействован в цикле, блокирует всю интерактивность (мы установили здесь число итераций 200 000, но это будет еще более очевидно, если мы увеличим его до 2 000 000).

Еще одна вещь, которая указывает нам на заблокированный основной поток, заключается в том, что рабочий процесс обновляет страницу на каждой итерации, а цикл в главном потоке (определенный в index.html ) обновляет #mainThreadOutput элемент #mainThreadOutput на последней итерации.

Это связано с тем, что браузер слишком занят подсчетом ( for цикла), чтобы иметь возможность перерисовывать DOM, поэтому он делает это только после того, как его работа с циклом for полностью завершена (в конце цикла).

Вывод

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

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

Есть ли у вас какие-либо советы относительно веб-работников и Интернета как платформы для программирования? Дайте нам знать об этом в комментариях!