От автора: сегодня поговорим о том, для чего нужен Web Worker JavaScript. В 2019 году веб-экосистема эволюционировала до такой степени, что браузер стал средой исполнения для приложений, построенных на JavaScript. Это выражается в скорости, с которой отрасль год за годом предлагает новые фреймворки, парадигмы, загрузчики и сборщики модулей, менеджеры зависимостей, инструменты сборки и менеджеры пакетов.
Когда JavaScript был задуман в первые дни Интернета, направление развития Сети было неясно. Из-за постоянных, быстрых изменений в отрасли и экосистеме, необходимости обратной совместимости браузеров и веб-стандартов развитие JavaScript стало постоянным потоком исправлений, хаков и последствий.
Современные мобильные устройства обычно поставляются с 8+ ядрами ЦП или 12+ ядрами GPU. Настольные и серверные процессоры имеют до 16 ядер или более. В этой среде узкое место — наличие доминирующей однопоточной среды программирования или скриптов.
JavaScript является однопоточным
Это означает, что по замыслу движки JavaScript — изначально браузеры — имеют один основной поток выполнения, или, проще говоря, процесс или функция B не могут быть выполнены до тех пор, пока процесс или функция A не будут завершены. Пользовательский интерфейс веб-страницы не реагирует на любую другую обработку JavaScript, когда он занят выполнением чего-либо — это называется блокировкой DOM.
Это ужасно неэффективно, особенно по сравнению с другими языками. Если мы перейдем на JS Bin и запустим этот код в консоли JavaScript браузера:
1 2 3 4 5 6 |
//noprotect i = 0; while (i < 60000) { console.log("The number is " + i); i++; } |
… весь сайт jsbin.com перестанет отвечать на запросы до тех пор, пока браузер не посчитает и не выведет в консоль до 60 000. Мы не сможем взаимодействовать с чем-либо на странице, потому что браузер занят. Это относительно нетребовательный вычислительный процесс, и современные веб-приложения часто включают в себя гораздо более сложные задачи. Нам нужно уметь вычислять вещи в фоновом режиме, пока пользователь без проблем взаимодействует со страницей.
Web Worker
W3C опубликовал первый проект стандарта Web Worker в 2009 году. С полной спецификацией можно ознакомиться на веб-сайте Рабочей группы по технологиям веб-гипертекстовых приложений — или WHATWG — альтернативной W3C организации по веб-стандартам.
Web Worker — это асинхронная система или протокол, позволяющий веб-страницам выполнять задачи в фоновом режиме независимо от основного потока и пользовательского интерфейса веб-сайта. Это изолированная среда, которая отделена от объекта window, объекта document, прямого доступа в Интернет и лучше всего подходит для длительных или сложных вычислительных задач.
Помимо Web Worker — системы, предназначенной для многопоточности — существуют и другие способы выполнения асинхронной обработки в JavaScript, такие как асинхронные вызовы Ajax и цикл обработки событий. Чтобы продемонстрировать это, мы вернемся к JS Bin и попробуем этот фрагмент кода:
1 2 3 4 5 6 |
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() в порядке указанных соответствующих задержек. Однако эта асинхронность не должна автоматически сопоставляться с многопоточностью. В зависимости от хост-машины это часто может быть просто однопоточный стек вызовов в порядке, который мы объяснили.
Web Worker и многопоточность
Как объясняет справочный веб-сайт Mozilla по JavaScript, Web Worker является «средством для веб-контента запускать скрипты в фоновых потоках».
Мы используем их следующим образом: проверяем наличие конструктора Worker() в браузере и, если он доступен, создаем экземпляр объекта worker с URL-адресом скрипта в качестве аргумента. Этот скрипт будет выполняться в отдельном потоке.
Скрипт должен обслуживаться с того же хоста или домена из соображений безопасности, и это также является причиной того, что Web Worker не будет работать, если мы откроем файл локально по схеме file://.
1 2 3 |
if (typeof(Worker) !== "undefined") { worker = new Worker("worker.js"); } |
Теперь мы определим этот код в файле worker.js:
1 2 3 4 5 |
i = 0; while (i < 200000) { postMessage("Web Worker Counter: " + i); i++; } |
Разделение потоков
Важным моментом здесь является разделение диапазонов выполнения window и document в главном потоке окна браузера, и диапазона worker.
Чтобы использовать поток worker, эти два диапазона должны быть в состоянии взаимодействовать. Чтобы добиться этого, мы используем в файле worker.js функцию postMessage() — для отправки сообщений в основной поток браузера — и прослушиватель worker.onmessage в основном потоке для прослушивания сообщений worker.
Мы также можем отправлять сообщения из основного потока браузера в поток или функцию worker. Единственное отличие состоит в том, что все происходит наоборот, и мы вызываем worker.postMessage() в основном потоке, а onmessage в потоке worker. Цитата из Mozilla:
Обратите внимание на то, что onmessage и postMessage() должны испускаться из объекта Worker при использовании в главном потоке скрипта, но не тогда, когда используются в Worker. Это связано с тем, что внутри Worker он фактически является глобальной областью действия.
Мы можем использовать метод terminate() таким же образом, чтобы завершить выполнение Worker. Имея все это в виду, рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
<!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:
1 2 3 4 5 |
i = 0; while (i < 200000) { postMessage("Web Worker Counter: " + i); i++; } |
Это дает нам возможность проверить влияние выполнения основного потока на поведение и производительность страницы по сравнению с влиянием web worker. В этом руководстве мы использовали http-server, чтобы обслуживать файлы локально.
Теперь мы можем видеть, что поток Worker не блокирует интерактивность основного процесса браузера, а циклический просмотр 200 000 чисел не влияет на основной поток. Числа в элементе #workerOutput обновляются при каждой итерации.
Блокирующий поток, или основной поток, когда задействован в цикле, блокирует всю интерактивность (здесь мы установили число итераций 200 000, но это будет еще более очевидно, если мы увеличим его до 2 000 000).
Еще одна вещь, которая указывает нам на заблокированный основной поток, заключается в том, что процесс Worker обновляет страницу при каждой итерации, а цикл в главном потоке (определенный в index.html) обновляет только элемент #mainThreadOutput на последней итерации.
Это связано с тем, что браузер слишком занят подсчетом (циклом for), чтобы иметь возможность перерисовывать DOM, поэтому он делает это только после того, как работа с циклом for завершена (в конце цикла).
Заключение
В этой статье мы представили Web Worker, технологию, которая помогает веб-индустрии идти в ногу со все более и более требовательными веб-приложениями. Это достигается путем предоставления веб-приложениям возможности использовать многопроцессорные и многопоточные устройства, добавляя в JavaScript некоторые многопоточные суперспособности.
Web Worker превращает мобильные и настольные браузерные среды в платформы приложений, предоставляя им среду строгого исполнения. Эта строгость может заставить обеспечивать копирование объектов между несколькими потоками и планировать наши приложения с учетом этих ограничений.
Есть ли у вас какие-либо советы, касающиеся Web Worker? Дайте нам знать об этом в комментариях!
Автор: Tonino Jankov
Источник: //www.sitepoint.com
Редакция: Команда webformyself.