Асинхронний JS, Fetch

Асинхронне завантаження і виконання зовнішніх скриптів

Розглянемо такий приклад:

...
<script src="js/counter.js"></script>
<script src="js/ad_box.js"></script>
<link type="text/css" rel="stylesheet" href="css/styles.css">
</head>
<body>
...
<script src=".../ga.js"></script>
<script src=".../ya.js"></script>
<script src=".../ra.js"></script>
<script src="js/jquery.min.js"></script>
<script>
  $('body').css('background', 'yellow');
  var h1 = document.querySelector('h1');
  h1.innerHTML = h1.innerHTML + '!!!';
</script>
</body></html>

В даному випадку всі події відбуваються синхронно:
- завантажується файл counter.js з лічильником відвідувачів;
- виконуються скрипти, що прописані в цьому файлі;
- завантажується файл ad_box.js, що відповідає за рекламу на сайті;
- виконуються скрипти, що прописані в цьому файлі;
- завантажуються стилі;
- лише зараз починає завантажуватися контент, користувач починає щось бачити на сторінці;
- завантажується скрипт аналітики гугла;
- виконується скрипт аналітики гугла, збирається статистика, відправляється на сервер;
- завантажується скрипт аналітики яндекса;
- виконується скрипт аналітики яндекса, збирається статистика, відправляється на сервер;
- завантажується скрипт аналітики рамблера;
- виконується скрипт аналітики рамблера, збирається статистика, відправляється на сервер;
- завантажується бібліотека jQuery;
- і лише тепер виконається наш скрипт, спочатку jQuery-залежна строка, і в самому кінці - дві строки на чистому JS.

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

Async.

Асинхронність дозволяє значно пришвидшити завантаження веб-сторінки, завантажуючи ресурси паралельно, не очікуючи один одного.

Для асинхронного завантаження використовується властивість тега async, добавимо її в перші два скрипта і скрипти аналітики:

...
<script src="js/counter.js" async></script>
<script src="js/ad_box.js" async></script>
...
<script async src=".../ga.js"></script>
<script async src=".../ya.js"></script>
<script async src=".../ra.js"></script>
...

Тепер наш сайт завантажується в такому порядку:
- завантажуються стилі, а в фоні завантажуються файли counter.js та ad_box.js, скрипти виконаються коли завантажиться файл;
- починає завантажуватися контент, користувач починає щось бачити на сторінці;
- завантажується бібліотека jQuery, а в фоні завантажуються скрипти аналітики (три штуки одночасно) і по ходу діла в паралельних потоках обраховують свою статистику;
- тепер виконається наш скрипт, спочатку jQuery-залежна строка, і в самому кінці - на чистому JS.

Проводимо оптимізацію швидкості далі. Останнім двом строчкам коду не потрібні бібліотеки, вони можуть бути виконані раніше:

...
<script>
  var h1 = document.querySelector('h1');
  h1.innerHTML = h1.innerHTML + '!!!';
</script>
<script async src=".../ga.js"></script>
...

Або ще вище по коду, їх можна виконати одразу після завантаження відповідного тега:

...
<h1>Заголовок сторінки</h1>
<script>
  var h1 = document.querySelector('h1');
  h1.innerHTML = h1.innerHTML + '!!!';
</script>
...

В принципі, і jQuery можна завантажувати асинхронно, в такому випадку завантаження бібліотеки не буде тормозити виконання цих останніх двох строчок:

...
<script src="js/jquery.min.js" async></script>
<script>
  $('body').css('background', 'yellow');
  var h1 = document.querySelector('h1');
  h1.innerHTML = h1.innerHTML + '!!!';
</script>
...

Але тут є підводний камінь: бібліотека почне завантажуватися в фоні і паралельно з нею виконається наступна строка коду. При цьому знак долара ще не буде асоційований з jQuery-функцією.

В такому випадку jQuery-код потрібно огорнути в подію load вікна:

...
<script src="js/jquery.min.js" async></script>
<script>
  window.addEventListener('load', function(){
    $('body').css('background', 'yellow');
  });
  var h1 = document.querySelector('h1');
  h1.innerHTML = h1.innerHTML + '!!!';
</script>
...

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

defer

Властивість тега defer також вказує на потребу в асинхронності завантаження скриптів, але не у їх виконанні (відкладене виконання).

По-перше, хоч скрипти і будуть завантажуватися паралельно, проте, виконаються лише в кінці завантаження всієї сторінки.

По-друге, буде дотримана послідовність виконання скриптів, що мають властивість defer.

Розглянемо на прикладі:

<script defer src="s1.js"></script>
<script async src="big.js"></script>
<script defer src="s2.js"></script>
<script async src="small.js"></script>
<script defer src="s3.js"></script>

Скрипт big.js - великий плагін на пару мегабайт.

Скрипт small.js - маленький файлик на декілька строк.

Порядок виконання: - всі 5 файлів починають завантажуватися одночасно в паралельних потоках*, не заважаючи завантаженню сторінки;
- файл small.js швиденько завантажиться і почне виконувати свій скрипт;
- якщо файл big.js завантажиться раніше, ніж завантажиться вся сторінка - він почне виконувати свій скрипт;
- пронумеровані скрипти завантажаться, дочекаються завантаження всієї сторінки і почнуть виконуватися в заданому порядку (порядок виконання визначає не цифра в назві, а порядок слідування в html-коді).

*) Насправді, сервер, як правило, обмежує кількість потоків, в чому можна переконатися дивлячись на сторінку з купою дрібних картинок-превюх, зазвичай вони завантажуються по 4 штуки паралельно, а не всі декілька десятків одночасно.

Увага! Властивість async не підтримується браузерами IE9-, в разі потреби продумайте логіку завантаження для застарілих браузерів.

Асинхронно можна завантажувати лише скрипти, для асинхронного завантаження стилів просто виконайте скрипт, який динамічно створить тег <link> з відповідним атрибутом href у блоці <head>.

Якщо вказати одночасно обидві властивості (порядок слідування не важливий):

<script async defer src="script1.js"></script>
<script defer src="script2.js" async></script>

то сучасні браузери проігнорують властивість defer і вважатимуть скрипт повністю асинхронним, а IE9- не знатиме що таке async і використає властивість defer.

Асинхронне виконання функцій

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

Всі реакції на події є також асинхронними і виконуються паралельно.

setTimeout і setInterval запускають свої каллбек-функції також запускають в паралельних потоках.

З асинхронним виконанням функцій потрібно бути обережним:

var i = 10;
setInterval(function(){ console.log(i++); }, 500);
setInterval(function(){ console.log(i--); }, 500);

Також у нас виникали проблеми зі слайдером через одночасне виконання функцій: автоматичне гортання і реакція на керування клавіш.

Відловлювати помилки у паралельних потоках, що використовують спільний простір імен, глобальні змінні - велика морока, можна заблукати у трьох строчках коду:

for (var i = 0; i < 10; i++){
  setTimeout(function(){ console.log(i); }, i * 1000);
}

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

Розпаралелюванням обчислень, процесів займаються об'ємні та наукоємкі галузі інформатики, математики, криптографії.

Проміси

В редакцію ES-2015 добавлено об'єкти-обіцянки (promise), за допомогою яких зручно виконувати асинхронні дії.

Проміс - це об'єкт, що містить стан виконання і функцію (яку отримав у якості аргумента під час створення).

Проміс виконує свою функцію і вона повертає результат асинхронно. При цьому в функції можна розділити потоки на вірне та помилкове виконання:

let promise = new Promise(function (funOk, funErr){
  if (...) funOk(your_ok_data);
  else funErr(your_err_data);
});

У проміса є методи .then(fun_ok, fun_err) та .catch(fun_err). Функції funOk, funErr можуть повертати значення, які автоматично будуть передані як аргументи в fun_ok та fun_err.

Помилки можна не обробляти, тоді друга функція не потрібна. Якщо ж немає потреби обробляти вірне виконання - замість функцій funOk чи fun_ok можна використати null, або ж використати метод .cathc(fun_err).

let promise = new Promise(function (funOk){
  let a = 10, b = 15;
  funOk({ a, b, state: 'ok' });
}).then(function (data){
  return [data.a, data.b];
}).then(function (arr){
  return 'a + b = ' + (arr[0] + arr[1]);
}).then(function (msg){
  alert(msg);
});

Давайте детально розберемо що тут відбувається:

  1. Створюємо проміс з функцією, в яку передається аргументом функція funOk.
  2. Функція виконує деякі дії та викликає функцію funOk, передає їй аргументом деякі дані. Звідки береться ця функція funOk?
  3. funOk - це анонімна функція function (data) { } з наступного метода .then(f), і аргумент data - це і будуть передані нами дані (в цьому випадку об'єкт з a, b та state).
  4. Метод .then(f) виконує деякі операції та може повернути певне значення, яке буде передане функції в наступному методі .then(f) (в цьому випадку функція повертає масив і передає його як змінну arr функції наступного .then()).
  5. Ну і так далі. Функція останнього .then(f) може нічого не повертати.

Поки що детальніше розгляньте проміси за статтею на learn.javascript.ru.

Fetch

Функція fetch заснована на промісах, заміняє XMLHttpRequest.

Браузери IE не підтримують fetch, використайте поліфіл, який буде емулювати цей метод в старих браузерах.

Функція повертає проміс:

// запит json-файла з сервера:
let promise = fetch('json/data.json');

Другим аргументом в функцію можна передати необхідні налаштування:

let url = 'hello.txt';
let options = {
  method: 'POST',
  body: 'login=user&pwd=12345'
  ...
}
fetch(url, options).then(funOk, funErr);

Опції:
method - метод передачі даних, GET чи POST;
headers - добавити заголовки запиту;
body - тіло запиту, дані, що будуть передані на сервер;
mode - режим кросдоменності;
credentials - передача додаткових даних про авторизацію, кукі;
cache - метод кешування запитів;
redirect - обробка редіректа.

На проміси калбеки навішуються за допомогою метода .then():

promise.then(funOk, funErr);
function funOk(){ console.log('All ok'); }
function funErr(){ console.log('Error!'); }

З функцією fetch в калбеки передаються змінні, куди буде записано дані - результат запита:

fetch(url, options)
  .then(fun1, fun2);

function fun1(response){
  console.log('all ok');
  h1.innerHTML = response.text();
}

function fun2(response){
  console.log('error: ' + response.status);
}

Результат відповіді сервера можна отримати в наступних форматах:
response.arrayBuffer();
response.blob();
response.formData();
response.json();
response.text();

В цілому робота з fetch виглядає таким чином:

// отримуємо html-код з файла index.html
fetch('index.html')
  .then(resp => resp.text())
  .then(function (html){
    // щось робимо з отриманим html-кодом
  });

// отримуємо JSON зі скрипта search.php
fetch('search.php?min_id=752&order=size')
  .then(resp => resp.json())
  .then(function (obj){
    // щось робимо з отриманим об'єктом
  });

// відправляємо на сервер POST з даними форми авторизації
login = encodeURIComponent(login);
password = encodeURIComponent(password);
let fetchOptions = {
  method: 'post',
  body: 'login=' + login + '&pwd=' + password,
  headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }
};
fetch('authorize.php', fetchOptions)
  .then(msg => msg.text())
  .then(alert);

Завдання.

  1. Отримайте за допомогою функції fetch дані з текстового файлу і вставте їх в <h1>.
  2. Отримайте дані з файлу JSON та вбудуйте їх у веб-сторінку: {
      "name": "Молоток",
      "descr": "Слюсарний молоток з дерев'яною ручкою",
      "count": 21,
      "price": 270
    }

    <p class="name">Назва товару: <span></span></p>
    <p class="descr">Опис: <span></span></p>
    <p class="count">Кількість на складі: <span></span> шт.</p>
    <p class="price">Ціна: <span></span> грн.</p>
  3. Отримайте файл files.json: ["a.txt", "b", "c.json"] Створіть три цих файли і в кожен запишіть масив з декількох довільних чисел.
    Створіть ланцюжк викликів: спочатку отримайте files.json, тоді - кожен з вказаних у ньому файлів та порахуйте суму усіх чисел у всіх файлах.
  4. Зверніться до неіснуючого файла та обробіть помилку.

Домашнє завдання.

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