Использование Web Workers
Web Workers — простой способ для веб-контента запускать скрипты в фоновых потоках. Поток работника может выполнять задачи без вмешательства в пользовательский интерфейс. Кроме того, они могут выполнять сетевые запросы с помощью API fetch() или XMLHttpRequest. После создания работник может отправлять сообщения JavaScript-коду, который его создал, публикуя сообщения в обработчик событий, указанный этим кодом (и наоборот).
Эта статья предоставляет подробное введение в использование Web Workers.
API Web Workers
Работник — это объект, созданный с помощью конструктора (например, Worker()), который выполняет именованный JavaScript-файл — этот файл содержит код, который будет выполняться в потоке работника; работники выполняются в другом глобальном контексте, отличном от текущего window. Таким образом, использование сокращения window для получения текущего глобального контекста (вместо self) в Worker вернёт ошибку.
Контекст работника представлен объектом DedicatedWorkerGlobalScope в случае выделенных работников (стандартные работники, используемые одним скриптом; общие работники используют SharedWorkerGlobalScope). К выделенному работнику можно получить доступ только из скрипта, который его впервые запустил, в то время как к общим работникам можно получить доступ из нескольких скриптов.
Примечание: См. Страница с документацией API Web Workers для справочной документации по работникам и дополнительных руководств.
Вы можете запускать любой код, который вам нравится, внутри потока работника, с некоторыми исключениями. Например, вы не можете напрямую манипулировать DOM изнутри работника или использовать некоторые стандартные методы и свойства объекта window. Но вы можете использовать большое количество элементов, доступных в window, включая WebSockets и механизмы хранения данных, такие как IndexedDB. Подробнее см. Функции и классы, доступные работникам.
Данные передаются между работниками и основным потоком через систему сообщений — обе стороны отправляют свои сообщения с помощью метода postMessage(), и отвечают на сообщения через обработчик событий onmessage (сообщение содержится в атрибуте данных события message). Данные копируются, а не разделяются.
Работники могут порождать новые работники, при условии, что эти работники размещены в том же домене, что и родительская страница.
Кроме того, работники могут выполнять сетевые запросы с помощью API fetch() или XMLHttpRequest (хотя обратите внимание, что атрибут responseXML объекта XMLHttpRequest всегда будет null).
Выделенные работники
Как упоминалось выше, к выделенному работнику можно получить доступ только из скрипта, который его вызвал. В этом разделе мы обсудим JavaScript, представленный в нашем примере выделенного работника (запустить выделенного работника): это позволяет ввести два числа, которые будут перемножены. Числа отправляются выделенному работнику, перемножаются, а результат возвращается на страницу и отображается.
Этот пример довольно тривиален, но мы решили сохранить его простым, знакомя вас с базовыми понятиями работников. Более сложные детали будут рассмотрены позже в статье.
Обнаружение возможностей работника
Для более контролируемой обработки ошибок и обратной совместимости рекомендуется заключать код доступа к работнику в следующее (main.js):
if (window.Worker) {
// …
}
Создание выделенного работника
Создание нового работника просто. Вам нужно только вызвать конструктор Worker(), указав URI скрипта, который нужно выполнить в потоке работника (main.js):
const myWorker = new Worker("worker.js");
Примечание: Сборка, включая webpack, Vite и Parcel, рекомендуют передавать URL, разрешённые относительно import.meta.url, конструктору Worker(). Например:
const myWorker = new Worker(new URL("worker.js", import.meta.url));
Таким образом, путь является относительным к текущему скрипту, а не к текущей HTML-странице, что позволяет сборщику безопасно выполнять оптимизации, такие как переименование (потому что в противном случае URL worker.js может указывать на файл, не контролируемый сборщиком, поэтому он не может делать никаких предположений).
Отправка сообщений выделенному работнику и обратно
Магия работников происходит через метод postMessage() и обработчик событий onmessage. Когда вы хотите отправить сообщение работнику, вы публикуете сообщения так (main.js):
[first, second].forEach((input) => {
input.onchange = () => {
myWorker.port.postMessage([first.value, second.value]);
console.log("Message posted to worker");
};
});
Итак, здесь у нас есть два элемента <input>, представленные переменными first и second; когда меняется значение любого из них, myWorker.postMessage([first.value,second.value]) используется для отправки значения обоих в работника, в виде массива. Вы можете отправлять практически всё, что угодно, в сообщении.
В работнике мы можем ответить на полученное сообщение, написав блок обработчика событий следующим образом (worker.js):
onmessage = (e) => {
console.log("Message received from main script");
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
console.log("Posting message back to main script");
postMessage(workerResult);
};
Обработчик onmessage позволяет нам выполнить некоторый код всякий раз, когда поступает сообщение, причём само сообщение доступно в атрибуте message события data. Здесь мы перемножаем два числа, а затем используем postMessage() снова, чтобы отправить результат обратно в основной поток.
Возвращаясь в основной поток, мы используем onmessage снова, чтобы ответить на сообщение, отправленное работником:
myWorker.onmessage = (e) => {
result.textContent = e.data;
console.log("Message received from worker");
};
Здесь мы получаем данные события сообщения и устанавливаем их как значение textContent абзаца с результатом, чтобы пользователь мог увидеть результат вычисления.
Примечание: Обратите внимание, что onmessage и postMessage() должны быть привязаны к объекту Worker при использовании в главном потоке скрипта, но не при использовании в работнике. Это связано с тем, что внутри работника работник фактически является глобальным контекстом.
Примечание: При передаче сообщения между основным потоком и работником оно копируется или «передаётся» (перемещается), а не делится. Подробнее об этой теме см. Передача данных работнику и обратно: дополнительные сведения.
Завершение работы работника
Если вам нужно немедленно завершить работу работающего работника из основного потока, вы можете сделать это, вызвав метод работника terminate:
myWorker.terminate();
Поток работника убивается немедленно.
Обработка ошибок
Когда возникает ошибка во время выполнения в работнике, вызывается его обработчик события onerror. Он получает событие с именем error, которое реализует интерфейс ErrorEvent.
Событие не «пузырится» и может быть отменено; чтобы предотвратить выполнение действия по умолчанию, работник может вызвать метод события об ошибке preventDefault().
Событие об ошибке имеет следующие три поля, которые представляют интерес:
Создание дочерних работников
Работники могут создавать дополнительные работники, если это необходимо. Так называемые дочерние работники должны размещаться в том же домене, что и родительская страница. Кроме того, URI дочерних работников разрешаются относительно местоположения родительского работника, а не родительской страницы. Это упрощает отслеживание зависимостей работников.
Импорт скриптов и библиотек
Потоки-рабочие имеют доступ к глобальной функции, importScripts(), которая позволяет им импортировать скрипты. Она принимает ноль или более URI в качестве параметров для ресурсов для импорта; все следующие примеры допустимы:
importScripts(); /* imports nothing */
importScripts("foo.js"); /* imports just "foo.js" */
importScripts("foo.js", "bar.js"); /* imports two scripts */
importScripts(
"//example.com/hello.js",
); /* You can import scripts from other origins */
Браузер загружает каждый указанный скрипт и выполняет его. Любые глобальные объекты из каждого скрипта могут затем использоваться рабочим потоком. Если скрипт не может быть загружен, NETWORK_ERROR выбрасывается, и последующий код не будет выполнен. Однако ранее выполненный код (включая код, отложенный с помощью setTimeout()) по-прежнему будет функциональным. Объявления функций после метода importScripts() также сохраняются, поскольку они всегда оцениваются до остальной части кода.
Примечание: Скрипты могут загружаться в любом порядке, но будут выполняться в том порядке, в котором вы передаете имена файлов в importScripts(). Это выполняется синхронно; importScripts() не возвращает значение, пока все скрипты не будут загружены и выполнены.
Общие рабочие потоки
Общий рабочий поток доступен нескольким скриптам — даже если к нему обращаются из разных окон, фреймов или даже рабочих потоков. В этом разделе мы обсудим JavaScript, найденный в нашем примере базового общего рабочего потока (запуск общего рабочего потока): Он очень похож на базовый пример выделенного рабочего потока, за исключением того, что он имеет две доступные функции, обрабатываемые различными файлами скриптов: умножение двух чисел или возведение числа в квадрат. Оба скрипта используют один и тот же рабочий поток для выполнения фактического вычисления, необходимого для решения задачи.
Здесь мы сосредоточимся на различиях между выделенными и общими рабочими потоками. Обратите внимание, что в этом примере у нас есть две HTML-страницы, каждая из которых имеет примененный JavaScript, использующий один и тот же единственный файл рабочего потока.
Примечание: Если к SharedWorker можно получить доступ из нескольких контекстов просмотра, все эти контексты просмотра должны разделять ровно один и тот же источник (один и тот же протокол, хост и порт).
Примечание: В Firefox общие рабочие потоки не могут быть разделены между документами, загруженными в частных и нечастных окнах (ошибка Firefox 1177621).
Создание общего рабочего потока
Создание нового общего рабочего потока практически такое же, как и с выделенным рабочим потоком, но с другим именем конструктора (см. index.html и index2.html) — каждый из них должен запустить рабочий поток, используя код, подобный следующему:
const myWorker = new SharedWorker("worker.js");
Существенное различие заключается в том, что с общим рабочим потоком вам необходимо взаимодействовать через объект port — явным образом открывается порт, который скрипты могут использовать для связи с рабочим потоком (в случае выделенных рабочих потоков это делается неявно).
Подключение порта должно быть начато либо неявно с помощью обработчика событий onmessage, либо явно с помощью метода start(), прежде чем можно будет отправлять сообщения. Вызов start() необходим только в том случае, если событие message подсоединено через метод addEventListener().
Примечание: При использовании метода start() для открытия подключения порта, его необходимо вызывать как из родительского потока, так и из потока рабочего потока, если требуется двусторонняя связь.
Отправка сообщений в общий рабочий поток и обратно
Теперь сообщения могут отправляться в рабочий поток, как и прежде, но метод postMessage() должен вызываться через объект порта (снова, вы увидите аналогичные конструкции как в multiply.js, так и в square.js):
squareNumber.onchange = () => {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log("Message posted to worker");
};
Теперь перейдем к рабочему потоку. Здесь также немного больше сложности (worker.js):
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (e) => {
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
port.postMessage(workerResult);
};
};
Во-первых, мы используем обработчик onconnect для запуска кода при подключении к порту (т. е. при настройке обработчика событий onmessage в родительском потоке или при явном вызове метода start() в родительском потоке).
Мы используем атрибут ports этого объекта события, чтобы получить порт и сохранить его в переменной.
Далее, мы добавляем обработчик onmessage на порту для выполнения вычисления и возврата результата в основной поток. Настройка этого обработчика onmessage в потоке рабочего потока также неявно открывает подключение порта обратно к родительскому потоку, поэтому вызов port.start() фактически не требуется, как отмечено выше.
Наконец, обратно в основном скрипте мы обрабатываем сообщение (снова, вы увидите аналогичные конструкции как в multiply.js, так и в square.js):
myWorker.port.onmessage = (e) => {
result2.textContent = e.data;
console.log("Message received from worker");
};
Когда сообщение возвращается через порт из рабочего потока, мы вставляем результат вычисления в соответствующий абзац результата.
О безопасности потоков
Интерфейс Worker порождает реальные потоки на уровне операционной системы, и внимательные программисты могут опасаться, что конкурентность может вызвать «интересные» эффекты в вашем коде, если вы не будете осторожны.
Однако, поскольку у веб-рабочих потоков тщательно контролируются точки взаимодействия с другими потоками, фактически очень сложно вызвать проблемы с параллельными процессами. Нет доступа к компонентам или DOM, которые не являются безопасными для параллельного выполнения. Вам необходимо передавать определенные данные в и из потока через сериализованные объекты. Таким образом, вам придется очень постараться, чтобы вызвать проблемы в вашем коде.
Политика безопасности содержимого
Рабочие потоки считаются имеющими свой собственный контекст выполнения, отличный от документа, который их создал. По этой причине они, как правило, не регулируются политикой безопасности содержимого (CSP) документа (или родительского рабочего потока), который их создал. Например, предположим, что документ обслуживается с помощью следующего заголовка:
Content-Security-Policy: script-src 'self'
В частности, это предотвратит использование любых включенных в него скриптов eval(). Однако, если скрипт создает рабочий поток, код, работающий в контексте рабочего потока, будет разрешен для использования eval().
Чтобы указать политику безопасности содержимого для рабочего потока, установите заголовок ответа Content-Security-Policy для запроса, который предоставил сам скрипт рабочего потока.
Исключением из этого правила является случай, когда происхождение скрипта рабочего потока — это глобально уникальный идентификатор (например, если его URL имеет схему data или blob). В этом случае рабочий поток наследует CSP документа или рабочего потока, который его создал.
Передача данных в рабочие потоки и обратно: дополнительные сведения
Данные, передаваемые между главной страницей и рабочими потоками, копируются, а не разделяются. Объекты сериализуются при передаче в рабочий поток и десериализуются на другом конце. Главная страница и рабочий поток не используют один и тот же экземпляр, поэтому в результате создается дубликат на каждом конце. Большинство браузеров реализуют эту функцию как структурированное клонирование.
Чтобы проиллюстрировать это, давайте создадим функцию под названием emulateMessage(), которая будет имитировать поведение значения, которое клонируется, а не делится при переходе из worker на главную страницу или наоборот:
function emulateMessage(vVal) {
return eval(`(${JSON.stringify(vVal)})`);
}
// Tests
// test #1
const example1 = new Number(3);
console.log(typeof example1); // object
console.log(typeof emulateMessage(example1)); // number
// test #2
const example2 = true;
console.log(typeof example2); // boolean
console.log(typeof emulateMessage(example2)); // boolean
// test #3
const example3 = new String("Hello World");
console.log(typeof example3); // object
console.log(typeof emulateMessage(example3)); // string
// test #4
const example4 = {
name: "Carina Anand",
age: 43,
};
console.log(typeof example4); // object
console.log(typeof emulateMessage(example4)); // object
// test #5
function Animal(type, age) {
this.type = type;
this.age = age;
}
const example5 = new Animal("Cat", 3);
alert(example5.constructor); // Animal
alert(emulateMessage(example5).constructor); // Object
Значение, которое клонируется и не делится, называется сообщением. Как вы, вероятно, уже знаете, сообщения можно отправлять в основной поток и из него, используя postMessage(), и атрибут data события message содержит данные, возвращенные из рабочего потока.
example.html (главная страница):
const myWorker = new Worker("my_task.js");
myWorker.onmessage = (event) => {
console.log(`Worker said : ${event.data}`);
};
myWorker.postMessage("ali");
my_task.js (рабочий поток):
postMessage("I'm working before postMessage('ali').");
onmessage = (event) => {
postMessage(`Hi, ${event.data}`);
};
Алгоритм структурированного клонирования может принимать JSON и несколько вещей, которые JSON не может — например, циклические ссылки.
Примеры передачи данных
Пример 1: Расширенная передача данных JSON и создание системы переключения
Если вам нужно передать сложные данные и вызвать множество различных функций как на главной странице, так и в Worker, вы можете создать систему, которая объединяет всё вместе.
Сначала мы создаём класс QueryableWorker, который принимает URL рабочего процесса, обработчик по умолчанию и обработчик ошибок, и этот класс будет отслеживать список обработчиков и помогать нам взаимодействовать с рабочим процессом:
function QueryableWorker(url, defaultListener, onError) {
const instance = this;
const worker = new Worker(url);
const listeners = {};
this.defaultListener = defaultListener ?? (() => {});
if (onError) {
worker.onerror = onError;
}
this.postMessage = (message) => {
worker.postMessage(message);
};
this.terminate = () => {
worker.terminate();
};
}
Затем мы добавляем методы добавления/удаления обработчиков:
this.addListeners = (name, listener) => {
listeners[name] = listener;
};
this.removeListeners = (name) => {
delete listeners[name];
};
Здесь мы позволяем рабочему процессу обрабатывать две простые операции для иллюстрации: вычисление разницы между двумя числами и вывод предупреждения через три секунды. Для этого мы сначала реализуем метод sendQuery, который проверяет, действительно ли рабочий процесс имеет соответствующие методы для выполнения того, что нам нужно.
// This functions takes at least one argument, the method name we want to query.
// Then we can pass in the arguments that the method needs.
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError(
"QueryableWorker.sendQuery takes at least one argument",
);
}
worker.postMessage({
queryMethod,
queryMethodArguments,
});
};
Мы завершаем QueryableWorker методом onmessage. Если рабочий процесс имеет соответствующие запрошенные методы, он должен вернуть имя соответствующего обработчика и аргументы, которые ему нужны, нам просто нужно найти его в listeners:
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethodListener") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
listeners[event.data.queryMethodListener].apply(
instance,
event.data.queryMethodArguments,
);
} else {
this.defaultListener.call(instance, event.data);
}
};
Теперь перейдём к рабочему процессу. Сначала нам нужно иметь методы для обработки двух простых операций:
const queryableFunctions = {
getDifference(a, b) {
reply("printStuff", a - b);
},
waitSomeTime() {
setTimeout(() => {
reply("doAlert", 3, "seconds");
}, 3000);
},
};
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError("reply - takes at least one argument");
}
postMessage({
queryMethodListener,
queryMethodArguments,
});
}
/* This method is called when main page calls QueryWorker's postMessage method directly*/
function defaultReply(message) {
// do something
}
И метод onmessage теперь тривиален:
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethod") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
queryableFunctions[event.data.queryMethod].apply(
self,
event.data.queryMethodArguments,
);
} else {
defaultReply(event.data);
}
};
Вот полная реализация:
example.html (главная страница):
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>MDN Example - Queryable worker</title>
<script type="text/javascript">
// QueryableWorker instances methods:
// * sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc.): calls a Worker's queryable function
// * postMessage(string or JSON Data): see Worker.prototype.postMessage()
// * terminate(): terminates the Worker
// * addListener(name, function): adds a listener
// * removeListener(name): removes a listener
// QueryableWorker instances properties:
// * defaultListener: the default listener executed only when the Worker calls the postMessage() function directly
function QueryableWorker(url, defaultListener, onError) {
const instance = this;
const worker = new Worker(url);
const listeners = {};
this.defaultListener = defaultListener ?? (() => {});
if (onError) {
worker.onerror = onError;
}
this.postMessage = (message) => {
worker.postMessage(message);
};
this.terminate = () => {
worker.terminate();
};
this.addListener = (name, listener) => {
listeners[name] = listener;
};
this.removeListener = (name) => {
delete listeners[name];
};
// This functions takes at least one argument, the method name we want to query.
// Then we can pass in the arguments that the method needs.
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError(
"QueryableWorker.sendQuery takes at least one argument",
);
}
worker.postMessage({
queryMethod,
queryMethodArguments,
});
};
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethodListener") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
listeners[event.data.queryMethodListener].apply(
instance,
event.data.queryMethodArguments,
);
} else {
this.defaultListener.call(instance, event.data);
}
};
}
// your custom "queryable" worker
const myTask = new QueryableWorker("my_task.js");
// your custom "listeners"
myTask.addListener("printStuff", (result) => {
document
.getElementById("firstLink")
.parentNode.appendChild(
document.createTextNode(`The difference is ${result}!`),
);
});
myTask.addListener("doAlert", (time, unit) => {
alert(`Worker waited for ${time} ${unit} :-)`);
});
</script>
</head>
<body>
<ul>
<li>
<a
id="firstLink"
href="javascript:myTask.sendQuery('getDifference', 5, 3);"
>What is the difference between 5 and 3?</a
>
</li>
<li>
<a href="javascript:myTask.sendQuery('waitSomeTime');"
>Wait 3 seconds</a
>
</li>
<li>
<a href="javascript:myTask.terminate();">terminate() the Worker</a>
</li>
</ul>
</body>
</html>
my_task.js (рабочий процесс):
const queryableFunctions = {
// example #1: get the difference between two numbers:
getDifference(minuend, subtrahend) {
reply("printStuff", minuend - subtrahend);
},
// example #2: wait three seconds
waitSomeTime() {
setTimeout(() => {
reply("doAlert", 3, "seconds");
}, 3000);
},
};
// system functions
function defaultReply(message) {
// your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
// do something
}
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError("reply - not enough arguments");
}
postMessage({
queryMethodListener,
queryMethodArguments,
});
}
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethod") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
queryableFunctions[event.data.queryMethod].apply(
self,
event.data.queryMethodArguments,
);
} else {
defaultReply(event.data);
}
};
Возможна смена содержимого сообщения между главной страницей и рабочим процессом. Имена свойств "queryMethod", "queryMethodListeners", "queryMethodArguments" могут быть любыми, при условии, что они согласованы в QueryableWorker и worker.
Передача данных путем передачи владения (передаваемые объекты)
Современные браузеры предоставляют дополнительный способ передачи определенных типов объектов рабочему процессу с высокой производительностью. Передаваемые объекты передаются из одного контекста в другой с помощью операции нулевого копирования, что приводит к значительному улучшению производительности при отправке больших наборов данных.
Например, при передаче ArrayBuffer из вашего основного приложения в скрипт рабочего процесса, исходный ArrayBuffer очищается и больше не используется. Его содержимое (буквально) передаётся в контекст рабочего процесса.
// Create a 32MB "file" and fill it with consecutive values from 0 to 255 – 32MB = 1024 * 1024 * 32 const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i); worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
Встроенные рабочие процессы
Нет «официального» способа встроить код рабочего процесса в веб-страницу, как это делают элементы <script> для обычных скриптов. Но элемент <script>, который не имеет атрибута src и имеет атрибут type, который не идентифицирует исполняемый MIME-тип, может рассматриваться как элемент блока данных, который JavaScript может использовать. «Блоки данных» — это более общая функция HTML, которая может содержать практически любые текстовые данные. Таким образом, рабочий процесс можно встроить следующим образом:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>MDN Example - Embedded worker</title>
<script type="text/js-worker">
// This script WON'T be parsed by JS engines because its MIME type is text/js-worker.
const myVar = 'Hello World!';
// Rest of your worker code goes here.
</script>
<script>
// This script WILL be parsed by JS engines because its MIME type is text/javascript.
function pageLog(sMsg) {
// Use a fragment: browser will only render/reflow once.
const frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode(sMsg));
frag.appendChild(document.createElement("br"));
document.querySelector("#logDisplay").appendChild(frag);
}
</script>
<script type="text/js-worker">
// This script WON'T be parsed by JS engines because its MIME type is text/js-worker.
onmessage = (event) => {
postMessage(myVar);
};
// Rest of your worker code goes here.
</script>
<script>
// This script WILL be parsed by JS engines because its MIME type is text/javascript.
// In the past blob builder existed, but now we use Blob
const blob = new Blob(
Array.prototype.map.call(
document.querySelectorAll("script[type='text\/js-worker']"),
(script) => script.textContent,
),
{ type: "text/javascript" },
);
// Creating a new document.worker property containing all our "text/js-worker" scripts.
document.worker = new Worker(window.URL.createObjectURL(blob));
document.worker.onmessage = (event) => {
pageLog(`Received: ${event.data}`);
};
// Start the worker.
window.onload = () => {
document.worker.postMessage("");
};
</script>
</head>
<body>
<div id="logDisplay"></div>
</body>
</html>
Встроенный рабочий процесс теперь вложен в новое пользовательское свойство document.worker.
Стоит также отметить, что вы также можете преобразовать функцию в Blob, а затем сгенерировать URL объекта из этого Blob. Например:
function fn2workerURL(fn) {
const blob = new Blob([`(${fn.toString()})()`], { type: "text/javascript" });
return URL.createObjectURL(blob);
}
Дополнительные примеры
В этом разделе приведены дополнительные примеры использования веб-рабочих процессов.
Выполнение вычислений в фоновом режиме
Рабочие процессы в основном полезны для выполнения ресурсоёмких вычислений без блокировки потока пользовательского интерфейса. В этом примере рабочий процесс используется для вычисления чисел Фибоначчи.
Код JavaScript
Следующий код JavaScript хранится в файле "fibonacci.js", на который ссылается HTML в следующем разделе.
self.onmessage = (event) => {
const userNum = Number(event.data);
self.postMessage(fibonacci(userNum));
};
function fibonacci(num) {
let a = 1;
let b = 0;
while (num > 0) {
[a, b] = [a + b, a];
num--;
}
return b;
}
Рабочий процесс устанавливает свойство onmessage в функцию, которая будет получать сообщения, отправленные при вызове postMessage() объекта рабочего процесса. Это выполняет математические вычисления и, в конечном итоге, возвращает результат обратно в основной поток.
Код HTML
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>Fibonacci number generator</title>
<style>
body {
width: 500px;
}
div,
p {
margin-bottom: 20px;
}
</style>
</head>
<body>
<form>
<div>
<label for="number"
>Enter a number that is a zero-based index position in the fibonacci
sequence to see what number is in that position. For example, enter 6
and you'll get a result of 8 — the fibonacci number at index position
6 is 8.</label
>
<input type="number" id="number" />
</div>
<div>
<input type="submit" />
</div>
</form>
<p id="result"></p>
<script>
const form = document.querySelector("form");
const input = document.querySelector('input[type="number"]');
const result = document.querySelector("p#result");
const worker = new Worker("fibonacci.js");
worker.onmessage = (event) => {
result.textContent = event.data;
console.log(`Got: ${event.data}`);
};
worker.onerror = (error) => {
console.log(`Worker error: ${error.message}`);
throw error;
};
form.onsubmit = (e) => {
e.preventDefault();
worker.postMessage(input.value);
input.value = "";
};
</script>
</body>
</html>
Веб-страница создаёт элемент <p> с ID result, который используется для отображения результата, затем запускает рабочий процесс. После запуска рабочего процесса обработчик onmessage настраивается для отображения результатов путём установки содержимого элемента <p>, а обработчик onerror настраивается на вывод сообщения об ошибке в консоли инструментов разработчика.
Наконец, отправляется сообщение рабочему процессу для его запуска.
Распределение задач между несколькими рабочими процессами
Поскольку многоядерные компьютеры становятся всё более распространёнными, часто бывает полезно распределить вычислительно сложные задачи между несколькими рабочими процессами, которые затем могут выполнять эти задачи на нескольких процессорных ядрах.
Другие типы рабочих процессов
Помимо специализированных и общих веб-рабочих процессов, существуют и другие типы рабочих процессов:
- ServiceWorkers по сути являются прокси-серверами, которые находятся между веб-приложениями и браузером и сетью (если доступна). Они предназначены (среди прочего) для создания эффективных автономных сред, перехватывая запросы к сети и принимая соответствующие действия в зависимости от того, доступна ли сеть и обновлены ли ресурсы на сервере. Они также позволят доступ к уведомлениям и API фонового синхронизации.
- Audio Worklet обеспечивают возможность прямого скриптового аудиообработки в контексте worklet (лёгкая версия рабочего процесса).
Отладка потоков рабочих процессов
Большинство браузеров позволяют отлаживать веб-рабочие процессы в своих отладчиках JavaScript точно так же, как и отладку основного потока! Например, как Firefox, так и Chrome отображают файлы исходного кода JavaScript как для основного потока, так и для активных потоков рабочих процессов, и все эти файлы можно открыть для установки точек останова и точек регистрации.
Чтобы узнать, как отлаживать веб-рабочие процессы, см. документацию по отладчику JavaScript каждого браузера:
Функции и интерфейсы, доступные в рабочих процессах
Вы можете использовать большинство стандартных функций JavaScript внутри веб-рабочего процесса, включая:
Главное, что вы не можете сделать в рабочем процессе — это напрямую повлиять на родительскую страницу. Это включает манипулирование DOM и использование объектов этой страницы. Вы должны делать это косвенно, отправляя сообщение обратно в основной скрипт через DedicatedWorkerGlobalScope.postMessage(), а затем выполняя изменения в обработчике событий.
Примечание: Вы можете проверить, доступен ли метод рабочим процессам, используя сайт: https://worker-playground.glitch.me/. Например, если вы введёте EventSource на сайт в Firefox 84, вы увидите, что это не поддерживается в service worker, но поддерживается в специализированных и общих рабочих процессах.
Примечание: Для получения полного списка функций, доступных рабочим процессам, см. Функции и интерфейсы, доступные рабочим процессам.
Спецификации
| Спецификация |
|---|
| HTML # рабочие процессы |
См. также
-
Workerинтерфейс -
SharedWorkerинтерфейс - Доступные рабочим процессам функции
-
OffscreenCanvasинтерфейс
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers