Веб-працівники в дії: чому вони корисні та як ви повинні ними користуватися

Javascript є однопотоковим, і кілька сценаріїв не можуть виконуватися одночасно. Отже, якщо ми виконуємо важкі обчислювальні завдання, то іноді наша сторінка не реагує, і користувач не може робити нічого іншого, поки це виконання не буде завершено.

Наприклад:

average = (numbers) => { let startTime = new Date().getTime(); let len = numbers, sum = 0, i; if (len === 0) { return 0; } for (i = 0; i  { alert("Hello World !!"); } /* Paste the above code in browser dev tool console and try to call average(10000) and hello one by one */

У наведеному вище прикладі, якщо ви зателефонуєте середньому перед методом hello , ваша сторінка стане невідповідною, і ви не зможете натиснути Hello, поки виконання середнього не завершиться.

Ви можете бачити, що коли середнє викликається з 10000 як введення першим, це займало ~ 1,82 секунди. Протягом такого періоду часу сторінка не відповідає, і ви не змогли натиснути кнопку привіт.

Асинхронне програмування

Javascript дає можливість розробникам писати асинхронний код . Написавши асинхронний код, ви можете уникнути подібних проблем у вашому додатку, оскільки це дозволяє інтерфейсу вашого додатка реагувати, "плануючи" частини коду, які будуть виконані трохи пізніше у циклі подій.

Хорошим прикладом асинхронного програмування є те XHR request, що в цьому випадку ми асинхронно натискаємо API, і, очікуючи відповіді, може бути виконаний інший код. Але це обмежується певними випадками використання, пов’язаними переважно з веб-API.

Інший спосіб написання асинхронного коду - це використання setTimeoutметод. У деяких випадках можна досягти хороших результатів у розблокуванні інтерфейсу від більш тривалих обчислень за допомогою setTimeout. Наприклад, шляхом складання складних обчислень в окремих setTimeoutвикликах.

Наприклад:

average = (numbers) => { let startTime = new Date().getTime(); var len = numbers, sum = 0, i; if (len === 0) { return 0; } let calculateSumAsync = (i) => { if (i  { sum += i; calculateSumAsync(i + 1); }, 0); } else { // The end of the array is reached so we're invoking the alert. let endTime = new Date().getTime(); alert('Average - ', sum / len); } }; calculateSumAsync(0); }; hello = () => { alert('Hello World !!') };

У цьому прикладі ви можете бачити, що після натискання кнопки « Обчислити середнє значення » ви все одно можете натиснути кнопку « Привіт » (яка, у свою чергу, відображає попереджувальне повідомлення). Цей спосіб програмування, безумовно, не блокує, але займає занадто багато часу і не є здійсненним у реальних програмах.

Тут для того самого вводу 10000 це зайняло ~ 60 секунд, що є дуже неефективним.

Отже, як ми ефективно вирішуємо такі проблеми?

Відповідь - веб-працівники.

Що таке веб-працівники?

Веб-співробітники в Javascript - це чудовий спосіб виконати якесь завдання, яке є дуже копітким і вимагає часу в потоці, окремому від основного потоку. Вони працюють у фоновому режимі і виконують завдання, не втручаючись в користувальницький інтерфейс.

Веб-співробітники не є частиною JavaScript, це функція браузера, до якої можна отримати доступ через JavaScript.

Веб-працівники створюються функцією конструктора Worker (), яка запускає іменований файл JS.

// create a dedicated web worker const myWorker = new Worker('worker.js');

Якщо вказаний файл існує, його буде завантажено асинхронно, а якщо ні, то працівник не зможе вийти з ладу, тому ваша програма все одно працюватиме у випадку 404.

Ми дізнаємось більше про створення та роботу веб-працівників у наступному розділі.

Робочий потік має власний контекст, і тому ви можете отримати доступ лише до вибраних функцій усередині робочого потоку, таких як - веб-сокети, індексована БД.

Існують деякі обмеження щодо веб-працівників -

  1. Ви не можете безпосередньо маніпулювати DOM зсередини працівника.
  2. Ви не можете використовувати деякі методи та властивості об'єкта вікна за замовчуванням, оскільки об'єкт вікна недоступний всередині робочого потоку.
  3. До контексту всередині робочого потоку можна отримати доступ через DedicatedWorkerGlobalScope або SharedWorkerGlobalScope, залежно від використання.

Особливості веб-працівників

Існує два типи веб-працівників -

  1. Виділений веб-працівник - відданий працівник доступний лише за сценарієм, який його назвав.
  2. Спільний веб-працівник - Спільний працівник доступний за допомогою декількох сценаріїв - навіть якщо вони отримують доступ з різних вікон, фреймів або навіть працівників.

Давайте обговоримо докладніше про ці два типи веб-працівників -

Створення веб-працівника

Створення майже однакове як для виділеного, так і для спільного веб-працівника.

Виділений веб-працівник

  • Створити нового працівника просто, просто зателефонуйте конструктору Worker і передайте шлях сценарію, який ви хочете виконати як робочий.
// create a dedicated web worker const myWorker = new Worker('worker.js');

Спільний веб-працівник:

  • Створення нового спільного працівника є майже таким самим, як і у спеціального працівника, але з іншим ім'ям конструктора.
// creating a shared web worker const mySharedWorker = new SharedWorker('worker.js');

Зв'язок між основним і робочим потоком

Зв'язок між головним потоком і робочим потоком відбувається через PostMessage метод і OnMessage обробника подій.

Виділений веб-працівник

У випадку відданого веб-працівника система зв'язку проста. Вам просто потрібно використовувати метод postMessage, коли ви хочете надіслати повідомлення працівникові.

(() => { // new worker let myWorker = new Worker('worker.js'); // event handler to recieve message from worker myWorker.onmessage = (e) => { document.getElementById('time').innerHTML = `${e.data.time} seconds`; }; let average = (numbers) => { // sending message to web worker with an argument myWorker.postMessage(numbers); } average(1000); })();

А всередині веб-працівника ви можете відповісти на отримане повідомлення, написавши блок обробника подій так:

onmessage = (e) => { let numbers = e.data; let startTime = new Date().getTime(); let len = numbers, sum = 0, i; if (len === 0) { return 0; } for (i = 0; i < len; i++) { sum += i; } let endTime = new Date().getTime(); postMessage({average: sum / len, time: ((endTime - startTime) / 1000)}) };

onmessageОброблювач дозволяє запускати код при кожному прийомі повідомлення.

Тут ми обчислюємо середнє число, а потім використовуємо postMessage()знову, щоб опублікувати результат назад в основний потік.

Як ви можете бачити в рядку 6 в main.js, ми використовували подію onmessage у екземплярі робітника. Отже, коли робочий потік використовує postMessage, onmessage в основному потоці запускається.

  • Спільний веб-працівник

    У випадку спільного веб-працівника, система зв'язку мало чим відрізняється. Оскільки один працівник ділиться між кількома сценаріями, нам потрібно спілкуватися через об'єкт порту екземпляра worker. Це робиться неявно у випадку відданих працівників. Вам потрібно використовувати метод postMessage щоразу, коли ви хочете надіслати повідомлення працівникові.

(() => { // new worker let myWorker = new Worker('worker.js'); // event handler to recieve message from worker myWorker.onmessage = (e) => { document.getElementById('time').innerHTML = `${e.data.time} seconds`; }; let average = (numbers) => { // sending message to web worker with an argument myWorker.postMessage(numbers); } average(1000);

Усередині веб-працівника ( main-shared-worker.js ) це трохи складно. По-перше, ми використовуємо onconnectобробник для запуску коду, коли відбувається підключення до порту ( рядок 2 ).

Ми використовуємо portsатрибут цього об'єкта події, щоб захопити порт і зберегти його у змінну ( рядок 4 ).

Далі ми додаємо messageобробник до порту для обчислення та повертаємо результат до основного потоку ( рядок 7 та рядок 25 ) таким чином:

onmessage = (e) => { let numbers = e.data; let startTime = new Date().getTime(); let len = numbers, sum = 0, i; if (len === 0) { return 0; } for (i = 0; i < len; i++) { sum += i; } let endTime = new Date().getTime(); postMessage({average: sum / len, time: ((endTime - startTime) / 1000)}) };

Termination of a web worker

If you need to immediately terminate a running worker from the main thread, you can do so by calling the worker’s terminate method:

// terminating a web worker instance myWorker.terminate();

The worker thread is killed immediately without an opportunity to complete its operations.

Spawning of web worker

Workers may spawn more workers if they wish. But they must be hosted within the same origin as the parent page.

Importing Scripts

Worker threads have access to a global function, importScripts(), which lets them import scripts.

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 */

Working Demo

We have discussed some of the approaches above to achieve async programming so that our UI doesn’t get blocked due to any heavy computational task. But there are some limitations to those approaches. So we can use web workers to solve these kind of problems efficiently.

Click here to run this live demo.

Here, you will see 3 sections:

  1. Blocking Code:

    When you click on calculate average, the loader does not display and after some time you see the final result and time taken. This is because as soon as the average method gets called, I have triggered the showLoader method also. But since JS is single threaded, it won’t execute showLoader until the execution of average gets completed. So, you won’t be able to see the loader in this case ever.

  2. Async Code:

    In this I tried to achieve the same functionality by using the setTimeout method and putting every function execution into an event loop. You will see the loader in this case, but the response takes time as compared to the method defined above.

  3. Web worker:

    This is an example of using a web worker. In this you will see the loader as soon as you click on calculate average and you will get a response in the same time as of method 1, for the same number.

You can access the source code for the same here.

Advanced concepts

There are some advanced concepts related to web workers. We won’t be discussing them in detail, but its good to know about them.

  1. Content Security Policy —

    Web workers have their own execution context independent of the document that created them and because of this reason they are not governed by the Content Security Policy of the parent thread/worker.

    The exception to this is if the worker script's origin is a globally unique identifier (for example, if its URL has a scheme of data or blob). In this case, the worker inherit the content security policy of the document or worker that created it.

  2. Transferring data to and from workers

    Data passed between main and worker thread is copied and not shared. Objects are serialized as they're handed to the worker, and subsequently, de-serialized on the other end. The page and worker do not share the same instance, so the end result is that a duplicate is created on each end.

    Browsers implemented Structured Cloning algorithm to achieve this.

  3. Embedded workers —

    You can also embed the code of worker inside a web page (html). For this you need to add a script tag without a src attribute and assign a non-executable MIME type to it, like this:

    embedded worker   // This script WON'T be parsed by JS engines because its MIME type is text/js-worker. var myVar = 'Hello World!'; // worker block function onmessage(e) { // worker code }    

There are a lot of use cases to use web workers in our application. I have just discussed a small scenario. Hope this helps you understand the concept of web workers.

[Links]

Github Repo : //github.com/bhushangoel/webworker-demo-1 Web worker in action : //bhushangoel.github.io/webworker-demo-1/JS demo showcase : //bhushangoel.github.io/

Thank you for reading.

Happy Learning :)

Originally published at www.thehungrybrain.com.