Теперь, несмотря на то, что javascript является однопоточным, он хорошо обрабатывает асинхронные операции, не блокируя основной поток, благодаря синергии между стеком вызовов, веб-API, очередью обратного вызова и циклом обработки событий. Но что, если код, блокирующий основной поток, не является асинхронной операцией? Что, если есть цикл while с пограничным случаем, который заставляет его работать вечно? Ваш стек вызовов всегда будет занят этой циклической операцией, и если в очереди обратного вызова есть какие-либо элементы, они останутся там на неопределенный срок. Если бы мы только могли перенести эти потенциально блокирующие поток задачи на отдельный рабочий поток, который не запускается в основном потоке. Вы уже можете видеть, куда это идет. Да, я говорю о веб-воркерах.

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

Видеоверсия этого поста доступна на Youtube.

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

JavaScript предоставляет два типа веб-воркеров: выделенные и общие.

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

Давайте рассмотрим пример выделенного веб-воркера на JavaScript, который выполняет ресурсоемкую задачу: вычисление простых чисел (наихудшим из возможных способов).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="my-btn">Click me!</button>
    <script>
        const calculatePrimes = () => {
            const primes = [];
            for (let i = 2; i <= 10000; i++) {
                let isPrime = true;
                for (let j = 2; j < i; j++) {
                    if (i % j === 0) {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime) primes.push(i);
            }
            return primes;
        }
        console.log(calculatePrimes())
    </script>
</body>
</html>

В этом примере мы вычисляем простые числа от 2 до 100000. Теперь, если вы запустите это в браузере, вы увидите, что кнопка не отображается на странице, пока вычисление не будет завершено. Вот что я имею в виду, когда говорю «блокировать основной поток».

Теперь, чтобы решить эту проблему, давайте воспользуемся worker.

//worker.js
onmessage = event => {
    if (event.data == "start") {
        const primes = [];
        for (let i = 2; i <= 100000; i++) {
            let isPrime = true;
              for (let j = 2; j < i; j++) {
                  if (i % j === 0) {
                      isPrime = false;
                      break;
                  }
              }
              if (isPrime) primes.push(i);
          }
        postMessage(primes);
    }
}

//script.js
const worker = new Worker("./worker.js")
const calculatePrimes = () => {
    worker.postMessage("start")
}
calculatePrimes()
worker.onmessage = event => {
    console.log(event.data);
}

Вместо того, чтобы вычислять простые числа внутри этого файла, мы создадим новый рабочий экземпляр и позволим ему выполнить вычисления. Экземпляр рабочего процесса будет представлять собой простой JS-файл, в котором будет выполняться сложный расчет.
Внутри функции calculatePrimes мы используем worker.postMessage, чтобы уведомить рабочий процесс о начале расчета. В конце внутри основного скрипта мы добавляем прослушиватель событий onmessage в экземпляр рабочего процесса, чтобы получить сообщение (простые числа) от рабочего процесса.
В файле рабочего процесса мы снова прикрепим событие onmessage слушатель. Он будет ждать стартового сообщения от основного скрипта, и когда он получит сообщение, он должен начать расчет. Поэтому вам придется скопировать вычисление внутри этого прослушивателя событий. В конце концов, вместо того, чтобы возвращать массив, мы отправим его обратно, используя метод postMessage, доступный глобально.

Теперь, если вы запустите это внутри браузера, блокировок быть не должно. Основной поток больше не блокируется, поэтому вы сразу получаете кнопку. Рабочий поток одновременно вычисляет простые числа и отправляет результат обратно в основной поток, как только это будет сделано.

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

Общий работник

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

Хорошо, давайте создадим Shared worker. Внутри нашего основного файла сценария мы будем использовать экземпляр SharedWorker для создания общего рабочего процесса. Одно большое отличие общего рабочего процесса заключается в том, что вы должны взаимодействовать через объект port — открывается явный порт, который сценарии могут использовать для связи с рабочим процессом. Это делается неявно в случае посвященных работников.

Чтобы запустить соединение порта между этим скриптом и рабочим процессом, мы можем использовать прослушиватель событий onmessage. Есть еще один способ сделать это явно — метод start, но onmessage не только помогает вам с вашими обратными вызовами, но и неявно создает соединение, так что это намного удобнее.

Итак, внутри основного файла скрипта вам придется изменить класс Worker на SharedWorker. Подключите порт к функциям postMessage, а также к функциям onmessage.

const worker = new SharedWorker("worker.js")
const calculatePrimes = () => {
    worker.port.postMessage("start")
}
calculatePrimes()
worker.port.onmessage = event => {
    console.log("Message received from the worker.", event.data)
}

Внутри рабочего файла нам также придется внести некоторые изменения. Во-первых, мы используем обработчик onconnect для запуска кода, когда происходит соединение с портом, поэтому в основном, когда установлен обработчик события onmessage в родительском потоке.
Мы используем атрибут ports этого объекта события, чтобы захватить порт и сохранить его в переменной. В остальном то же самое. Только то, что события postMessage и onmessage будут выполняться на порту, поэтому просто добавьте его в обе ваши функции. Теперь, если вы запустите это в браузере, вы увидите, что все работает так, как ожидалось.

onconnect = e => {
    const port = e.ports[0];
    port.onmessage = event => {
        if (event.data == "start") {
            const primes = [];
            for (let i = 2; i <= 100000; i++) {
                let isPrime = true;

                for (let j = 2; j < i; j++) {
                    if (i % j === 0) {
                        isPrime = false;
                        break;
                    }
                }

                if (isPrime) primes.push(i);
            }

            port.postMessage(primes);
        }
    }
}

Теперь давайте создадим еще один скрипт (index2), который будет использовать тот же общий рабочий процесс для некоторых других вычислений. Просто скопируйте весь скрипт из первого файла и добавьте его во второй файл. Вместо отправки start мы отправим «start2», чтобы рабочий процесс различал сценарии.

Внутри воркера вы можете добавить еще один оператор if, который будет заниматься вычислениями для второго скрипта. Теперь эта реализация может быть намного чище, но я просто хочу показать вам, как вы можете настроить воркер для выполнения разных задач на основе входных данных от разных воркеров. Давайте просто отправим обратно сообщение, ничего не делая для этого работника. port.postMessage("Heavy calculation")

onconnect = e => {
    const port = e.ports[0];
    port.onmessage = event => {
        if (event.data == "start") {
            const primes = [];
            for (let i = 2; i <= 100000; i++) {
                let isPrime = true;

                for (let j = 2; j < i; j++) {
                    if (i % j === 0) {
                        isPrime = false;
                        break;
                    }
                }

                if (isPrime) primes.push(i);
            }

            port.postMessage(primes);
        }else if(event.data == "start2") {
            port.postMessage("Heavy calculation");
        }
    }
}

Теперь, если вы зайдете в браузер и откроете index2.html внутри localhost, вы найдете оператор console.log, который исходит от того же работника. Таким образом, мы по сути разделяем этого воркера из 2 разных скриптов для 2 разных вычислений.

Заключение

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

Видеоверсия этого поста доступна на Youtube.

YouTube
LinkedIn
Twitter
GitHub

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .