Одной из многих целей разработки языка JavaScript было сделать его однопоточным и, как следствие, простым. Хотя я должен признать, что, учитывая особенности языковых конструкций, это совсем не просто! Но то, что мы подразумеваем под «однопоточностью», это то, что в JavaScript есть только один поток управления; да, к сожалению, ваш движок JavaScript может делать только одно за раз.
Разве это не звучит слишком ограниченно, чтобы использовать многоядерные процессоры, бездействующие на вашем компьютере? HTML5 обещает изменить все это.
Однопоточная модель JavaScript
Веб-работники живут в ограниченном мире без доступа DOM, так как DOM не является поточно-ориентированным.
Одна школа мысли рассматривает однопоточную природу JavaScript как упрощение, а другая отклоняет его как ограничение. Последняя группа имеет очень хороший момент, особенно когда современные веб-приложения интенсивно используют JavaScript для обработки событий пользовательского интерфейса, запроса или опроса серверных API, обработки больших объемов данных и управления DOM на основе ответа сервера.
Быть способным сделать так много в одном потоке управления при поддержании отзывчивого пользовательского интерфейса часто является непростой задачей, и это заставляет разработчиков прибегать к взлому и обходным путям (таким как использование setTimeout()
, setInterval()
или использование XMLHttpRequest
и DOM события) для достижения параллелизма. Тем не менее, стоит отметить, что эти методы определенно предоставляют способ выполнять асинхронные вызовы, но неблокирование не обязательно означает одновременность. Джон Резиг объясняет, почему вы не можете запустить что-либо параллельно в своем блоге .
Ограничения
Если вы работали с JavaScript в течение разумного периода времени, весьма вероятно, что вы столкнулись со следующим раздражающим диалоговым окном, в котором говорится, что выполнение какого-либо сценария занимает слишком много времени. Да, почти каждый раз, когда ваша страница перестает отвечать, причину можно объяснить каким-то кодом JavaScript.
Вот некоторые из причин, по которым ваш браузер может просто прекратить загрузку при выполнении вашего скрипта:
- Чрезмерное манипулирование DOM : манипулирование DOM, пожалуй, самая дорогая операция, которую вы можете сделать с JavaScript. Следовательно, большое количество операций с DOM делает ваш скрипт хорошим кандидатом на рефакторинг.
- Бесконечные циклы : сканирование вашего кода на наличие сложных вложенных циклов никогда не повредит. Они, как правило, выполняют гораздо больше работы, чем нужно на самом деле. Возможно, вы можете найти другое решение, которое обеспечивает ту же функциональность.
- Комбинируя два : худшее, что мы можем сделать, — это неоднократно обновлять DOM в цикле, когда существуют более элегантные решения, такие как использование DocumentFragment .
Веб-работники на помощь
… неблокирование не обязательно означает одновременный …
Благодаря HTML5 и Web Workers вы можете создавать новый поток, обеспечивающий истинную асинхронность. Новый рабочий может работать в фоновом режиме, пока основной поток обрабатывает события пользовательского интерфейса, даже если рабочий поток занят обработкой большого количества данных. Например, рабочий может обработать большую структуру JSON, чтобы извлечь ценную информацию для отображения в пользовательском интерфейсе. Но хватит моего болтовни; давайте посмотрим код в действии.
Создание работника
Обычно код, относящийся к веб-работнику, находится в отдельном файле JavaScript. Родительский поток создает нового работника, указав URI файла сценария в конструкторе Worker
, который асинхронно загружает и выполняет файл JavaScript.
1
|
var primeWorker = new Worker(‘prime.js’);
|
Начать работника
Чтобы запустить работника, родительский поток отправляет сообщение работнику, например:
1
2
|
var current = $(‘#prime’).attr(‘value’);
primeWorker.postMessage(current);
|
Родительская страница может общаться с работниками с помощью API postMessage
, который также используется для обмена сообщениями между источниками. Помимо отправки примитивных типов данных работнику, API postMessage
также поддерживает передачу структур JSON. Однако нельзя передавать функции, поскольку они могут содержать ссылки на базовый DOM.
Родительский и рабочий потоки имеют свое отдельное пространство; сообщения, передаваемые туда и сюда, копируются, а не передаются.
За кулисами эти сообщения сериализуются на рабочем, а затем десериализуются на принимающей стороне. По этой причине не рекомендуется отправлять огромные объемы данных работнику.
Родительский поток также может зарегистрировать обратный вызов для прослушивания любых сообщений, которые рабочий отправляет обратно после выполнения своей задачи. Это позволяет родительскому потоку предпринять необходимые действия (например, обновить DOM) после того, как работник сыграет свою роль. Взгляните на этот код:
1
2
3
4
|
primeWorker.addEventListener(‘message’, function(event){
console.log(‘Receiving from Worker: ‘+event.data);
$(‘#prime’).html( event.data );
});
|
Объект event
содержит два важных свойства:
-
target
: используется для идентификации работника, отправившего сообщение; в первую очередь полезен в среде с несколькими работниками. -
data
: сообщение, отправленное рабочим обратно в его родительский поток.
Сам работник содержится в prime.js
и регистрирует событие message
, которое он получает от своего родителя. Он также использует тот же API postMessage
для связи с родительским потоком.
01
02
03
04
05
06
07
08
09
10
|
self.addEventListener(‘message’, function(event){
var currPrime = event.data, nextPrime;
setInterval( function(){
nextPrime = getNextPrime(currPrime);
postMessage(nextPrime);
currPrime = nextPrime;
}, 500);
});
|
Веб-работники живут в ограниченной и поточно-ориентированной среде.
В этом примере мы просто находим следующее наибольшее простое число и многократно публикуем результаты обратно в родительский поток, который, в свою очередь, обновляет пользовательский интерфейс с новым значением. В контексте работника, и self
и this
относятся к глобальной области. Рабочий может либо добавить прослушиватель события для события message
, либо определить обработчик onmessage
для прослушивания любых сообщений, отправленных родительским потоком.
Задача поиска следующего простого числа, очевидно, не является идеальным вариантом использования для работника, но была выбрана здесь для демонстрации концепции передачи сообщений. Позже мы рассмотрим возможные и практические варианты использования, где использование Web Worker действительно принесет пользу.
Увольнение работников
Работники ресурсоемки; это потоки уровня ОС. Поэтому вы не хотите создавать большое количество рабочих потоков, и вам следует прекратить работу веб-работника после того, как он завершит свою работу. Рабочие могут прекратить себя, как это:
1
|
self.close();
|
Или родительский поток может завершить рабочий:
1
|
primeWorker.terminate();
|
Безопасность и ограничения
Внутри рабочего скрипта у нас нет доступа ко многим важным объектам JavaScript, таким как document
, window
, console
, parent
и, что самое важное, нет доступа к DOM. Отсутствие доступа к DOM и невозможность обновить страницу звучит слишком ограничительно, но это важное решение для обеспечения безопасности. Только представьте, какой хаос это может вызвать, если несколько потоков попытаются обновить один и тот же элемент. Таким образом, веб-работники живут в ограниченной и поточно-ориентированной среде.
Сказав это, вы все равно можете использовать работников для обработки данных и возврата результата обратно в основной поток, который затем может обновить DOM. Хотя им отказано в доступе к некоторым довольно важным объектам JavaScript, работникам разрешено использовать некоторые функции, такие как setTimeout()/clearTimeout()
, setInterval()/clearInterval()
, navigator
и т. Д. Вы также можете использовать объекты XMLHttpRequest
и localStorage
внутри работник.
Те же ограничения происхождения
В контексте работника, и
self
иthis
относятся к глобальной области.
Для связи с сервером работники должны следовать той же политике происхождения . Например, сценарий, размещенный на http://www.example.com/
не может получить доступ к сценарию на https://www.example.com/
. Несмотря на то, что имена хостов совпадают, оригинальная политика гласит, что протокол должен быть таким же. Обычно это не проблема. Весьма вероятно, что вы пишете и рабочему, и клиенту, и обслуживаете их из одного домена, но знание ограничения всегда полезно.
Проблемы локального доступа с Google Chrome
Google Chrome накладывает ограничения на локальный доступ к сотрудникам, поэтому вы не сможете запускать эти примеры в локальной среде. Если вы хотите использовать Chrome, то вы должны либо разместить эти файлы на каком-либо сервере, либо использовать --allow-file-access-from-files
при запуске Chrome из командной строки. Для OS X запустите Chrome следующим образом:
1
|
$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome —allow-file-access-from-files
|
Однако использование этого флага не рекомендуется в производственной среде. Таким образом, вам лучше всего разместить эти файлы на веб-сервере и протестировать своих веб-работников в любом поддерживаемом браузере.
Отладка рабочих и обработка ошибок
Отсутствие доступа к console
делает это несколько нетривиальным, но благодаря Chrome Developer Tools можно отлаживать рабочий код, как если бы это был любой другой код JavaScript.
Для обработки любых ошибок, выдаваемых веб-работниками, вы можете прослушивать событие error
, которое заполняет объект ErrorEvent . Вы можете проверить этот объект, чтобы узнать подробную причину ошибки.
1
2
3
4
5
|
primeWorker.addEventListener(‘error’, function(error){
console.log(‘ Error Caused by worker: ‘+error.filename
+ ‘ at line number: ‘+error.lineno
+ ‘ Detailed Message: ‘+error.message);
});
|
Несколько рабочих потоков
Хотя обычно несколько рабочих потоков разделяют работу между собой, нужно быть осторожным. Официальная спецификация указывает, что эти рабочие относительно тяжелые и, как ожидается, будут долгоживущими сценариями, работающими в фоновом режиме. Веб-работники не предназначены для использования в больших количествах из-за их высокой производительности при запуске и высокой стоимости памяти для каждого экземпляра.
Краткое введение в общих работников
Спецификация выделяет два типа работников: выделенные и совместно используемые. До сих пор мы видели примеры преданных работников. Они напрямую связаны со своим сценарием / страницей создателя в том смысле, что они имеют отношение один к одному со сценарием / страницей, которая их создала. Совместно используемые работники, с другой стороны, могут совместно использоваться всеми страницами из источника (то есть: все страницы или сценарии одного и того же источника могут взаимодействовать с совместно используемым работником).
Чтобы создать общего работника, просто передайте URL-адрес сценария или имя работника в конструктор SharedWorker .
Основное различие в использовании общих работников заключается в том, что они связаны с port
для отслеживания доступа к ним родительского сценария.
Следующий фрагмент кода создает общего работника, регистрирует обратный вызов для прослушивания любых сообщений, отправленных работником, и отправляет сообщение общему работнику:
1
2
3
4
5
6
|
var sharedWorker = new SharedWorker(‘findPrime.js’);
sharedWorker.port.onmessage = function(event){
…
}
sharedWorker.port.postMessage(‘data you want to send’);
|
Аналогично, работник может прослушивать событие connect
, которое принимается, когда новый клиент пытается подключиться к работнику, и затем соответствующим образом отправляет ему сообщение.
01
02
03
04
05
06
07
08
09
10
11
12
|
onconnect = function(event) {
// event.source contains the reference to the client’s port
var clientPort = event.source;
// listen for any messages send my this client
clientPort.onmessage = function(event) {
// event.data contains the message send by client
var data = event.data;
….
// Post Data after processing
clientPort.postMessage(‘processed data’);
}
};
|
Из-за их общего характера вы можете поддерживать одно и то же состояние на разных вкладках одного и того же приложения, так как обе страницы на разных вкладках используют один и тот же сценарий общего рабочего, чтобы поддерживать и сообщать о состоянии. Для получения более подробной информации об общих работниках, я призываю вас прочитать спецификацию .
Практические варианты использования
Веб-работники не предназначены для использования в больших количествах из-за их высокой производительности при запуске и высокой стоимости памяти для каждого экземпляра.
Реальный сценарий может быть, когда вы вынуждены иметь дело с синхронным сторонним API, который заставляет основной поток ждать результата, прежде чем перейти к следующему оператору. В таком случае вы можете делегировать эту задачу вновь порожденному работнику, чтобы использовать асинхронные возможности в свою пользу.
Веб-работники также преуспевают в ситуациях опроса, когда вы постоянно опрашиваете адресата в фоновом режиме и отправляете сообщение в главный поток при поступлении новых данных.
Вам также может понадобиться обработать огромное количество данных, возвращаемых сервером. Традиционно обработка большого количества данных отрицательно влияет на скорость отклика приложения, что делает неприемлемым взаимодействие с пользователем. Более элегантное решение разделило бы работу по обработке между несколькими работниками для обработки непересекающихся частей данных.
Другими вариантами использования могут быть анализ видео или аудио источников с помощью нескольких веб-работников, каждый из которых работает над определенной частью проблемы.
Вывод
Представьте себе силу, связанную с несколькими потоками в однопоточной среде.
Как и многие вещи в спецификации HTML5, спецификация веб-работника продолжает развиваться. Если вы планируете работать в сети, вам не помешает взглянуть на спецификацию .
Кросс-браузерная поддержка довольно хороша для преданных своему делу сотрудников с текущими версиями Chrome, Safari и Firefox. Даже IE не сильно отстает от IE10, взяв на себя ответственность. Однако общие работники поддерживаются только в текущих версиях Chrome и Safari. Удивительно, но последняя версия браузера Android, доступная в Android 4.0, не поддерживает веб-работников, хотя они были поддержаны в версии 2.1. Apple также включила поддержку веб-работников, начиная с iOS 5.0.
Представьте себе мощность, связанную с несколькими потоками в однопоточной среде. Возможности безграничны!