From d53bae971e455859229bcb3246e29bcbc85179d2 Mon Sep 17 00:00:00 2001 From: MDN Date: Tue, 8 Mar 2022 00:12:08 +0000 Subject: [CRON] sync translated content --- files/ru/_redirects.txt | 5 +- files/ru/_wikihistory.json | 36 +- .../learn/javascript/asynchronous/index.html | 639 +++++++++++++++++++++ .../javascript/asynchronous/introducing/index.html | 165 ++++++ .../javascript/asynchronous/async_await/index.html | 415 ------------- .../javascript/asynchronous/concepts/index.html | 164 ------ .../javascript/asynchronous/promises/index.html | 416 ++++++++++++++ .../asynchronous/timeouts_and_intervals/index.html | 639 --------------------- 8 files changed, 1242 insertions(+), 1237 deletions(-) create mode 100644 files/ru/conflicting/learn/javascript/asynchronous/index.html create mode 100644 files/ru/conflicting/learn/javascript/asynchronous/introducing/index.html delete mode 100644 files/ru/learn/javascript/asynchronous/async_await/index.html delete mode 100644 files/ru/learn/javascript/asynchronous/concepts/index.html create mode 100644 files/ru/learn/javascript/asynchronous/promises/index.html delete mode 100644 files/ru/learn/javascript/asynchronous/timeouts_and_intervals/index.html (limited to 'files/ru') diff --git a/files/ru/_redirects.txt b/files/ru/_redirects.txt index aa50f26b06..377cf27043 100644 --- a/files/ru/_redirects.txt +++ b/files/ru/_redirects.txt @@ -221,7 +221,10 @@ /ru/docs/Learn/HTML/Введение_в_HTML/Структура_документа_и_веб-сайта /ru/docs/Learn/HTML/Introduction_to_HTML/Document_and_website_structure /ru/docs/Learn/HTML/Рецепты /ru/docs/Learn/HTML/Howto /ru/docs/Learn/How_the_Internet_works /ru/docs/Learn/Common_questions/How_does_the_Internet_work -/ru/docs/Learn/JavaScript/Asynchronous/Таймауты_и_интервалы /ru/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals +/ru/docs/Learn/JavaScript/Asynchronous/Async_await /ru/docs/Learn/JavaScript/Asynchronous/Promises +/ru/docs/Learn/JavaScript/Asynchronous/Concepts /ru/docs/conflicting/Learn/JavaScript/Asynchronous/Introducing +/ru/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals /ru/docs/conflicting/Learn/JavaScript/Asynchronous +/ru/docs/Learn/JavaScript/Asynchronous/Таймауты_и_интервалы /ru/docs/conflicting/Learn/JavaScript/Asynchronous /ru/docs/Learn/JavaScript/Building_blocks/События /ru/docs/Learn/JavaScript/Building_blocks/Events /ru/docs/Learn/JavaScript/Objects/Inheritance /ru/docs/Learn/JavaScript/Objects/Classes_in_JavaScript /ru/docs/Learn/JavaScript/Objects/Object-oriented_JS /ru/docs/conflicting/Learn/JavaScript/Objects/Classes_in_JavaScript diff --git a/files/ru/_wikihistory.json b/files/ru/_wikihistory.json index 4a8d00b762..d6c88836f4 100644 --- a/files/ru/_wikihistory.json +++ b/files/ru/_wikihistory.json @@ -3137,21 +3137,6 @@ "Sheppy" ] }, - "Learn/JavaScript/Asynchronous/Async_await": { - "modified": "2020-12-10T12:33:00.159Z", - "contributors": [ - "WhoRlyCares", - "Bucido" - ] - }, - "Learn/JavaScript/Asynchronous/Concepts": { - "modified": "2020-12-02T05:41:10.583Z", - "contributors": [ - "haltaction", - "KrasnovaM", - "snayp" - ] - }, "Learn/JavaScript/Asynchronous/Introducing": { "modified": "2020-12-09T10:05:03.071Z", "contributors": [ @@ -3165,11 +3150,11 @@ "MaxYenot" ] }, - "Learn/JavaScript/Asynchronous/Timeouts_and_intervals": { - "modified": "2020-12-09T10:32:13.319Z", + "Learn/JavaScript/Asynchronous/Promises": { + "modified": "2020-12-10T12:33:00.159Z", "contributors": [ "WhoRlyCares", - "velheor24" + "Bucido" ] }, "Learn/JavaScript/Building_blocks": { @@ -23981,6 +23966,21 @@ "TuchaNK" ] }, + "conflicting/Learn/JavaScript/Asynchronous": { + "modified": "2020-12-09T10:32:13.319Z", + "contributors": [ + "WhoRlyCares", + "velheor24" + ] + }, + "conflicting/Learn/JavaScript/Asynchronous/Introducing": { + "modified": "2020-12-02T05:41:10.583Z", + "contributors": [ + "haltaction", + "KrasnovaM", + "snayp" + ] + }, "conflicting/Learn/JavaScript/Objects": { "modified": "2019-06-04T15:16:30.349Z", "contributors": [ diff --git a/files/ru/conflicting/learn/javascript/asynchronous/index.html b/files/ru/conflicting/learn/javascript/asynchronous/index.html new file mode 100644 index 0000000000..f87839f2ff --- /dev/null +++ b/files/ru/conflicting/learn/javascript/asynchronous/index.html @@ -0,0 +1,639 @@ +--- +title: 'Объединённый асинхронный JavaScript: Таймауты и интервалы' +slug: conflicting/Learn/JavaScript/Asynchronous +translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +original_slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}
+ +

В этом руководстве рассматриваются традиционные методы, доступные в JavaScript для асинхронного выполнения кода по истечении заданного периода времени или через регулярный интервал (например, заданное количество раз в секунду), обсуждаются их полезные свойства и рассматриваются присущие им проблемы. .

+ + + + + + + + + + + + +
Необходимые условия:Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель:Понимание асинхронных циклов и интервалов, и то как их можно использовать.
+ +

Введение

+ +

В течение долгого времени веб-платформа предлагала программистам JavaScript ряд функций, которые позволяли им асинхронно выполнять код по истечении определённого временного интервала и повторно выполнять асинхронный блок кода, пока вы не скажете ему остановиться.

+ +

Эти функции:

+ +
+
setTimeout()
+
Выполняет указанный блок кода один раз по истечении указанного времени
+
setInterval()
+
Выполняет указанный блок кода несколько раз с определённым интервалом между каждым вызовом.
+
requestAnimationFrame()
+
Современная версия setInterval (). Выполняют указанный блок кода перед тем, как браузер в следующий раз перерисовывает отображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.
+
+ +

Асинхронный код, установленный этими функциями, выполняется в основном потоке (по истечении указанного им таймера).

+ +
+

Важно знать, что вы можете (и часто будете) запускать другой код до выполнения вызова setTimeout () или между итерациями setInterval (). В зависимости от того, насколько интенсивно используются эти операции для процессора, они могут ещё больше задержать выполнение асинхронного кода, поскольку любой асинхронный код будет выполняться только после того, как станет доступен основной поток. (Другими словами, когда стек пуст.) вы узнаете больше по этому вопросу по мере изучения этой статьи.

+
+ +

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

+ +

setTimeout()

+ +

Как мы ранее отметили, setTimeout () выполняет определённый блок кода один раз по истечении заданного времени. Принимает следующие параметры:

+ + + +
+

NOTE:  Указанное время (или задержка) не является гарантированным временем выполнения, а скорее минимальным временем выполнения. Обратные вызовы, которые вы передаёте этим функциям, не могут выполняться, пока стек в основном потоке не станет пустым.

+ +

Как следствие, такой код, как setTimeout (fn, 0), будет выполняться, как только стек будет пуст, а не сразу. Если вы выполните такой код, как setTimeout (fn, 0), но сразу после выполнения цикла, который насчитывает от 1 до 10 миллиардов, ваш колбэк будет выполнен через несколько секунд.

+
+ +

В следующем примере, браузер будет ожидать две секунды перед тем как  выполнит анонимную функцию, тогда отобразит сообщение (живой пример, и исходный код):

+ +
let myGreeting = setTimeout(function() {
+  alert('Hello, Mr. Universe!');
+}, 2000)
+ +

Указанные вами функции не обязательно должны быть анонимными. Вы можете дать своей функции имя и даже определить её где-нибудь ещё и передать ссылку на функцию в setTimeout (). Следующие две версии фрагмента кода эквивалентны первой:

+ +
// С именованной функцией
+let myGreeting = setTimeout(function sayHi() {
+  alert('Hello, Mr. Universe!');
+}, 2000)
+
+// С функцией определённой отдельно
+function sayHi() {
+  alert('Hello Mr. Universe!');
+}
+
+let myGreeting = setTimeout(sayHi, 2000);
+ +

Это может быть полезно, если у вас есть функция, которую нужно вызывать как по таймауту, так например и в ответ на событие. Но это также может  помочь поддерживать ваш код в чистоте, особенно если колбэк тайм-аута занимает больше, чем несколько строк кода.

+ +

setTimeout () возвращает значение идентификатора, которое можно использовать для ссылки на тайм-аут позже, например, когда вы хотите его остановить.

+ +

Передача параметров в функцию setTimeout ()

+ +

Любые параметры, которые вы хотите передать функции, выполняемой внутри setTimeout (), должны быть переданы ей как дополнительные параметры в конце списка.

+ +

Например, вы можете реорганизовать предыдущую функцию, чтобы она передавала привет любому имени, переданному ей:

+ +
function sayHi(who) {
+  alert(`Hello ${who}!`);
+}
+ +

Теперь вы можете передать имя в вызов setTimeout () в качестве третьего параметра:

+ +
let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');
+ +

Очистка таймаутов

+ +

Наконец, если был создан тайм-аут, вы можете отменить его до истечения указанного времени, вызвав clearTimeout(), передав ему идентификатор вызова setTimeout() в качестве параметра. Итак, чтобы отменить указанный выше тайм-аут, вы должны сделать следующее:

+ +
clearTimeout(myGreeting);
+ +
+

Note: См.greeter-app.html для более полной демонстрации, которая позволяет вам указать имя для приветствия и отменить приветствие с помощью отдельной кнопки (см. исходный код).

+
+ +

setInterval()

+ +

setTimeout () отлично работает, когда вам нужно один раз запустить код по истечении заданного периода времени. Но что происходит, когда вам нужно запускать код снова и снова - например, в случае анимации?

+ +

Здесь пригодится setInterval() . Работает очень похоже на setTimeout (), за исключением того, что функция, которую вы передаёте в качестве первого параметра, выполняется повторно не менее чем за количество миллисекунд, заданных вторым параметром. Вы также можете передать любые параметры, необходимые для выполняемой функции, в качестве последующих параметров вызова setInterval ().

+ +

Давайте посмотрим на пример. Следующая функция создаёт новый объект Date(), с помощью toLocaleTimeString() извлекает из него строку с временем и отображает её в пользовательском интерфейсе. Затем он запускает функцию один раз в секунду с помощью setInterval(), создавая эффект цифровых часов, которые обновляются раз в секунду ( реальный пример, и исходный код):

+ +
function displayTime() {
+   let date = new Date();
+   let time = date.toLocaleTimeString();
+   document.getElementById('demo').textContent = time;
+}
+
+const createClock = setInterval(displayTime, 1000);
+ +

Как и setTimeout (), setInterval () возвращает определённое значение, которое вы можете использовать позже, когда вам нужно очистить интервал.

+ +

Очистка интервала

+ +

setInterval () выполняет задачу постоянно. setInterval () продолжает выполнять задачу вечно, если вы что-то с ней не сделаете. Возможно, вам понадобится способ остановить такие задачи, иначе вы можете получить ошибки, если браузер не сможет выполнить какие-либо другие версии задачи или если анимация, обрабатываемая задачей, завершилась. Вы можете сделать это так же, как останавливаете timeouts - передавая идентификатор, возвращаемый вызовом setInterval (), в функцию clearInterval ():

+ +
const myInterval = setInterval(myFunction, 2000);
+
+clearInterval(myInterval);
+ +

Активное обучение: Создание собственного секундомера!

+ +

Учитывая все вышесказанное, у нас есть для вас задача. Возьмите копию нашего примера setInterval-clock.html , и измените её так, чтобы создать свой собственный простой секундомер.

+ +

Вам нужно отображать время, как и раньше, но в этом примере вам нужно:

+ + + +

Несколько подсказок для вас:

+ + + +
+

Note: Если вы застряли, вы можете увидеть нашу версию (см. также исходный код ).

+
+ +

Что нужно помнить о setTimeout () и setInterval ()

+ +

При работе с setTimeout () и setInterval () следует помнить о нескольких вещах. Давайте рассмотрим их.

+ +

Рекурсивные таймауты

+ +

Есть ещё один способ использования setTimeout (): вы можете вызвать его рекурсивно для повторного запуска одного и того же кода вместо использования setInterval ().

+ +

В приведённом ниже примере используется рекурсивный setTimeout () для запуска переданной функции каждые 100 миллисекунд:

+ +
let i = 1;
+
+setTimeout(function run() {
+  console.log(i);
+  i++;
+  setTimeout(run, 100);
+}, 100);
+ +

Сравните приведённый выше пример со следующим - здесь используется setInterval () для достижения того же эффекта:

+ +
let i = 1;
+
+setInterval(function run() {
+  console.log(i);
+  i++
+}, 100);
+ +

Чем рекурсивный setTimeout () отличается от setInterval () ?

+ +

Разница между двумя версиями приведённого выше кода невелика.

+ + + +

Когда ваш код потенциально может занять больше времени, чем назначенный вами интервал времени, лучше использовать рекурсивный setTimeout () - это сохранит постоянный временной интервал между выполнениями независимо от того, сколько времени потребуется для выполнения кода, и вы избежите ошибок.

+ +

Немедленные таймауты

+ +

Использование 0 в качестве значения для setTimeout () позволяет планировать выполнение указанной колбэк-функции как можно скорее, но только после того, как будет запущен основной поток кода.

+ +

Например, код приведённый ниже (рабочий код) выводит alert содержащий "Hello", затем alert содержащий "World" как только вы нажмёте ОК в первом alert.

+ +
setTimeout(function() {
+  alert('World');
+}, 0);
+
+alert('Hello');
+ +

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

+ +

Очистка с помощью clearTimeout() или clearInterval()

+ +

clearTimeout () и clearInterval () используют один и тот же список записей для очистки. Интересно, что это означает, что вы можете использовать любой метод для очистки setTimeout () или setInterval ().

+ +

Для согласованности следует использовать clearTimeout () для очистки записей setTimeout () и clearInterval () для очистки записей setInterval (). Это поможет избежать путаницы.

+ +

requestAnimationFrame()

+ +

requestAnimationFrame() это специализированная функция цикла, созданная для эффективного запуска анимации в браузере. По сути, это современная версия setInterval () - она выполняет указанный блок кода до того, как браузер перерисовывает изображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.

+ +

Он был создан в ответ на проблемы с setInterval (), который, например, не работает с частотой кадров, оптимизированной для устройства, иногда пропускает кадры, продолжает работать, даже если вкладка не является активной вкладкой или анимация прокручивается со страницы и т. д.(Читай об этом больше в CreativeJS.)

+ +
+

Note: вы можете найти примеры использования requestAnimationFrame() в этом курсе — например в  Рисование графики, and Практика построения объектов.

+
+ +

Метод принимает в качестве аргумента колбэк, который должен быть вызван перед перерисовкой. Это общий шаблон, в котором он используется:

+ +
function draw() {
+   // Drawing code goes here
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

Идея состоит в том, чтобы определить функцию, в которой ваша анимация обновляется (например, ваши спрайты перемещаются, счёт обновляется, данные обновляются или что-то ещё). Затем вы вызываете его, чтобы начать процесс. В конце функционального блока вы вызываете requestAnimationFrame () со ссылкой на функцию, переданной в качестве параметра, и это даёт браузеру указание вызвать функцию снова при следующей перерисовке дисплея. Затем он выполняется непрерывно, поскольку код рекурсивно вызывает requestAnimationFrame ().

+ +
+

Note: Если вы хотите выполнить простое постоянное анимирование DOM , CSS Анимация вероятно будет быстрее. Она вычисляется непосредственно внутренним кодом браузера, а не JavaScript.

+ +

Однако, если вы делаете что-то более сложное, включающее объекты, которые не доступны напрямую в the DOM (такие как 2D Canvas API или WebGL ), requestAnimationFrame() предпочтительный вариант в большинстве случаев.

+
+ +

Как быстро работает ваша анимация?

+ +

Плавность анимации напрямую зависит от частоты кадров анимации и измеряется в кадрах в секунду (fps). Чем выше это число, тем плавное будет выглядеть ваша анимация до точки.

+ +

Поскольку большинство экранов имеют частоту обновления 60 Гц, максимальная частота кадров, к которой вы можете стремиться, составляет 60 кадров в секунду (FPS) при работе с веб-браузерами. Однако большее количество кадров означает больше обработки, которая часто может вызывать заикание и пропуски, также известные как пропадание кадров или заедание.

+ +

Если у вас есть монитор с частотой обновления 60 Гц и вы хотите достичь 60 кадров в секунду, у вас есть около 16,7 миллисекунд (1000/60) для выполнения кода анимации для рендеринга каждого кадра. Это напоминание о том, что вам нужно помнить об объёме кода, который вы пытаетесь запустить во время каждого прохождения цикла анимации.

+ +

requestAnimationFrame () всегда пытается приблизиться к этому волшебному значению 60 FPS, насколько это возможно. Иногда это невозможно - если у вас действительно сложная анимация и вы запускаете её на медленном компьютере, частота кадров будет меньше. Во всех случаях requestAnimationFrame () всегда будет делать все возможное с тем, что у него есть.

+ +

Чем отличается requestAnimationFrame() от setInterval() and setTimeout()?

+ +

Давайте поговорим ещё немного о том, чем метод requestAnimationFrame () отличается от других методов, используемых ранее. Глядя на наш код сверху:

+ +
function draw() {
+   // Drawing code goes here
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

Такой же код с использованием setInterval():

+ +
function draw() {
+   // Drawing code goes here
+}
+
+setInterval(draw, 17);
+ +

Как мы уже говорили ранее, вы не указываете временной интервал для requestAnimationFrame (). Просто он работает максимально быстро и плавно в текущих условиях. Браузер также не тратит время на запуск, если по какой-то причине анимация выходит за пределы экрана и т. д.

+ +

setInterval (), с другой стороны, требует указания интервала. Мы пришли к нашему окончательному значению 17 по формуле 1000 миллисекунд / 60 Гц, а затем округлили его в большую сторону. Округление - хорошая идея; если вы округлите в меньшую сторону, браузер может попытаться запустить анимацию со скоростью, превышающей 60 кадров в секунду, и в любом случае это не повлияет на плавность анимации. Как мы уже говорили, стандартная частота обновления - 60 Гц.

+ +

В том числе временная метка

+ +

Фактическому колбэку, переданному в функцию requestAnimationFrame (), также может быть задан параметр: значение отметки времени, которое представляет время с момента начала работы requestAnimationFrame ().

+ +

Это полезно, поскольку позволяет запускать вещи в определённое время и в постоянном темпе, независимо от того, насколько быстрым или медленным может быть ваше устройство. Общий шаблон, который вы бы использовали, выглядит примерно так:

+ +
let startTime = null;
+
+function draw(timestamp) {
+    if (!startTime) {
+      startTime = timestamp;
+    }
+
+   currentTime = timestamp - startTime;
+
+   // Do something based on current time
+
+   requestAnimationFrame(draw);
+}
+
+draw();
+ +

Поддержка браузерами

+ +

requestAnimationFrame () поддерживается в более поздних версиях браузеров, чем setInterval () / setTimeout (). Интересно, что он доступен в Internet Explorer 10 и выше.

+ +

Итак, если вам не требуется поддержка старых версий IE, нет особых причин не использовать requestAnimationFrame().

+ +

Простой пример

+ +

Хватит теории! Давайте выполним упражнение с использованием requestAnimationFrame() . Создадим простую анимацию "spinner animation"—вы могли её видеть в приложениях когда происходят задержки при ответе с сервера и т.п..

+ +
+

Note: Для такой простой анимации, вам следовало бы использовать CSS . Однако такой вид анимации очень полезен для демонстрации requestAnimationFrame() , вы скорее всего будете использовать этот метод когда делаете что-то более сложное, например обновление отображения игры в каждом кадре.

+
+ +
    +
  1. +

    Возьмите базовый HTML шаблон (такой как этот).

    +
  2. +
  3. +

    Поместите пустой  {{htmlelement("div")}} элемент внутри элемента {{htmlelement("body")}}, затем добавьте внутрь символ ↻ . Этот символ будет действовать как spinner в нашем примере.

    +
  4. +
  5. +

    Примените следующий CSS к HTML шаблону (любым предпочитаемым способом). Он установ красный фон на странице, высоту <body> равную 100% высоты {{htmlelement("html")}} , и центрирует <div> внутри <body>, по горизонтали и вертикали.

    + +
    html {
    +  background-color: white;
    +  height: 100%;
    +}
    +
    +body {
    +  height: inherit;
    +  background-color: red;
    +  margin: 0;
    +  display: flex;
    +  justify-content: center;
    +  align-items: center;
    +}
    +
    +div {
    +  display: inline-block;
    +  font-size: 10rem;
    +}
    +
  6. +
  7. +

    Разместите  {{htmlelement("script")}} элемент перед </body> .

    +
  8. +
  9. +

    Разместите следующий JavaScript-код в  <script> . Здесь вы сохраняете ссылку на <div> внутри, устанавливаете для переменной rotateCount значение 0, устанавливаете неинициализированную переменную, которая позже будет использоваться для хранения ссылки на вызов requestAnimationFrame(), и устанавливаете для переменной startTime значение null, которая будет позже использоваться для хранения времени начала requestAnimationFrame().

    + +
    const spinner = document.querySelector('div');
    +let rotateCount = 0;
    +let startTime = null;
    +let rAF;
    +
    +
  10. +
  11. +

    Под предыдущим кодом вставьте функцию draw() которая будет использоваться для хранения нашего кода анимации, который включает параметр timestamp :

    + +
    function draw(timestamp) {
    +
    +}
    +
  12. +
  13. +

    Внутри draw () добавьте следующие строки. Они определят время начала, если оно ещё не определено (это произойдёт только на первой итерации цикла), и установят для параметра rotateCount значение для поворота счётчика (текущая временная метка, возьмите начальную временную метку, разделённую на три, чтобы замедлиться):

    + +
      if (!startTime) {
    +   startTime = timestamp;
    +  }
    +
    +  rotateCount = (timestamp - startTime) / 3;
    +
    +
  14. +
  15. +

    Под предыдущей строкой внутри draw () добавьте следующий блок - он проверяет, превышает ли значение rotateCount 359 (например, 360, полный круг). Если это так, он устанавливает значение по модулю 360 (то есть остаток, оставшийся после деления значения на 360), поэтому круговая анимация может продолжаться непрерывно с разумным низким значением. Обратите внимание, что это не является строго необходимым, но легче работать со значениями от 0 до 359 градусов, чем со значениями типа «128000 градусов».

    + +
    if (rotateCount > 359) {
    +  rotateCount %= 360;
    +}
    +
  16. +
  17. Затем, под предыдущим блоком, добавьте следующую строку, чтобы вращать spinner: +
    spinner.style.transform = `rotate(${rotateCount}deg)`;
    +
  18. +
  19. +

    В самом низу внутри функции draw () вставьте следующую строку. Это ключ ко всей операции - вы устанавливаете для переменной, определённой ранее, активный вызов requestAnimation (), который принимает функцию draw () в качестве своего параметра. Это запускает анимацию, постоянно выполняя функцию draw () со скоростью, близкой к 60 FPS.

    + +
    rAF = requestAnimationFrame(draw);
    +
  20. +
  21. +

    Ниже, вызовите функцию draw() для запуска анимации.

    + +
    draw();
    +
  22. +
+ +
+

Note: вы можете посмотреть рабочий образец на GitHub. ( исходный код.)

+
+ +

Очистка вызова  requestAnimationFrame() 

+ +

Очистить вызов requestAnimationFrame () можно, вызвав соответствующий метод cancelAnimationFrame (). (Обратите внимание, что имя функции начинается с «cancel», а не «clear», как у методов «set ...».)

+ +

Просто передайте ему значение, возвращаемое вызовом requestAnimationFrame () для отмены, которое вы сохранили в переменной rAF:

+ +
cancelAnimationFrame(rAF);
+ +

Активное обучение: запуск и остановка нашей анимации

+ +

В этом упражнении мы хотели бы, чтобы вы протестировали метод cancelAnimationFrame (), взяв наш предыдущий пример и обновив его, добавив обработчик событий для запуска и остановки счётчика при щелчке мышью в любом месте страницы.

+ +

Подсказки:

+ + + +
+

Note: Для начала попробуйте сами; если вы действительно застряли, посмотрите наш живой пример и исходный код.

+
+ +

Регулировка анимации requestAnimationFrame() 

+ +

Одним из ограничений requestAnimationFrame () является то, что вы не можете выбирать частоту кадров. В большинстве случаев это не проблема, так как обычно вы хотите, чтобы ваша анимация работала как можно плавное. Но как насчёт того, чтобы создать олдскульную 8-битную анимацию?

+ +

Это было проблемой, например в анимации ходьбы, вдохновлённой островом обезьян, из статьи Drawing Graphics:

+ +

{{EmbedGHLiveSample("learning-area/javascript/apis/drawing-graphics/loops_animation/7_canvas_walking_animation.html", '100%', 260)}}

+ +

В этом примере вы должны анимировать как положение персонажа на экране, так и отображаемый спрайт. В анимации спрайта всего 6 кадров. Если бы вы показывали разные кадры спрайта для каждого кадра, отображаемого на экране, с помощью requestAnimationFrame (), Guybrush двигал бы конечностями слишком быстро, и анимация выглядела бы нелепо. Следовательно, в этом примере регулируется скорость, с которой спрайт циклически повторяет свои кадры, используя следующий код:

+ +
if (posX % 13 === 0) {
+  if (sprite === 5) {
+    sprite = 0;
+  } else {
+    sprite++;
+  }
+}
+ +

Таким образом, код циклически повторяет спрайт только один раз каждые 13 кадров анимации.

+ +

... Фактически, это примерно каждые 6,5 кадров, поскольку мы обновляем posX (положение персонажа на экране) на два кадра:

+ +
if (posX > width/2) {
+  newStartPos = -( (width/2) + 102 );
+  posX = Math.ceil(newStartPos / 13) * 13;
+  console.log(posX);
+} else {
+  posX += 2;
+}
+ +

Это код, который вычисляет, как обновлять позицию в каждом кадре анимации.

+ +

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

+ +

Активное обучение: игра на реакцию

+ +

В последнем разделе этой статьи вы создадите игру на реакцию для двух игроков. В игре будет два игрока, один из которых управляет игрой с помощью клавиши A, а другой - с помощью клавиши L.

+ +

При нажатии кнопки «Start» счётчик, подобный тому, что мы видели ранее, отображается в течение случайного промежутка времени от 5 до 10 секунд. По истечении этого времени появится сообщение «PLAYERS GO !!» - как только это произойдёт, первый игрок, который нажмёт свою кнопку управления, выиграет игру.

+ +

{{EmbedGHLiveSample("learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html", '100%', 500)}}

+ +

Давайте поработаем над этим:

+ +
    +
  1. +

    Прежде всего, скачайте стартовый файл. Он содержит законченную структуру HTML и стили CSS, что даёт нам игровую доску, которая показывает информацию двух игроков (как показано выше), но с счётчиком и параграфом результатов, отображаемыми друг над другом. Вам нужно просто написать JavaScript-код.

    +
  2. +
  3. +

    Внутри пустого элемента {{htmlelement("script")}} на вашей странице, начните с добавления следующих строк кода, которые определяют некоторые переменные и константы, которые вам понадобятся в дальнейшем:

    + +
    const spinner = document.querySelector('.spinner p');
    +const spinnerContainer = document.querySelector('.spinner');
    +let rotateCount = 0;
    +let startTime = null;
    +let rAF;
    +const btn = document.querySelector('button');
    +const result = document.querySelector('.result');
    + +

    В следующем порядке:

    + +
      +
    1. Ссылка на спиннер, чтобы вы могли его анимировать.
    2. +
    3. Ссылка на элемент {{htmlelement("div")}} содержащий спиннер, используемый для отображения и скрытия.
    4. +
    5. Счётчик поворотов. Он определяет, на сколько вы хотите показывать вращение спиннера на каждом кадре анимации.
    6. +
    7. Нулевое время начала. Это будет заполнено временем начала, когда счётчик начнёт вращаться.
    8. +
    9. Неинициализированная переменная для последующего хранения вызова {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} который анимирует спиннер.
    10. +
    11. Ссылка на кнопку Start .
    12. +
    13. Ссылка на параграф результатов.
    14. +
    +
  4. +
  5. +

    Ниже добавьте следующую функцию. Она просто берёт два числа и возвращает случайное число между ними. Это понадобится вам позже, чтобы сгенерировать случайный интервал ожидания.

    + +
    function random(min,max) {
    +  var num = Math.floor(Math.random()*(max-min)) + min;
    +  return num;
    +}
    +
  6. +
  7. +

    Затем добавьте функцию draw(), которая анимирует спиннер. Это очень похоже на версию из предыдущего примера простого счётчика:

    + +
    function draw(timestamp) {
    +  if(!startTime) {
    +   startTime = timestamp;
    +  }
    +
    +  rotateCount = (timestamp - startTime) / 3;
    +
    +  if(rotateCount > 359) {
    +    rotateCount %= 360;
    +  }
    +
    +  spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
    +  rAF = requestAnimationFrame(draw);
    +}
    +
  8. +
  9. +

    Теперь пришло время настроить начальное состояние приложения при первой загрузке страницы. Добавьте следующие две строки, которые просто скрывают абзац результатов и контейнер счётчика с помощью display: none ;.

    + +
    result.style.display = 'none';
    +spinnerContainer.style.display = 'none';
    +
  10. +
  11. +

    Затем определите функцию reset (), которая возвращает приложение в исходное состояние, необходимое для повторного запуска игры после её завершения. Добавьте в конец кода следующее:

    + +
    function reset() {
    +  btn.style.display = 'block';
    +  result.textContent = '';
    +  result.style.display = 'none';
    +}
    +
  12. +
  13. +

    Хорошо, хватит подготовки! Пришло время сделать игру доступной! Добавьте в свой код следующий блок. Функция start () вызывает draw (), чтобы запустить вращение спиннера и отобразить его в пользовательском интерфейсе, скрыть кнопку Start, чтобы вы не могли испортить игру, запустив её несколько раз одновременно, и запускает вызов setTimeout (), который выполняется функция setEndgame () по прошествии случайного интервала от 5 до 10 секунд. Следующий блок также добавляет обработчик событий к вашей кнопке для запуска функции start () при её нажатии.

    + +
    btn.addEventListener('click', start);
    +
    +function start() {
    +  draw();
    +  spinnerContainer.style.display = 'block';
    +  btn.style.display = 'none';
    +  setTimeout(setEndgame, random(5000,10000));
    +}
    + +
    +

    Note: вы увидите, что этот пример вызывает setTimeout() без сохранения возвращаемого значения. (не  let myTimeout = setTimeout(functionName, interval).) 

    + +

    Это прекрасно работает, если вам не нужно очищать интервал / тайм-аут в любой момент. Если вы это сделаете, вам нужно будет сохранить возвращённый идентификатор!

    +
    + +

    Конечным результатом предыдущего кода является то, что при нажатии кнопки «Start» отображается спиннер, и игроки вынуждены ждать произвольное количество времени, прежде чем их попросят нажать их кнопку. Эта последняя часть обрабатывается функцией setEndgame (), которую вы определите позже.

    +
  14. +
  15. +

    Добавьте в свой код следующую функцию:

    + +
    function setEndgame() {
    +  cancelAnimationFrame(rAF);
    +  spinnerContainer.style.display = 'none';
    +  result.style.display = 'block';
    +  result.textContent = 'PLAYERS GO!!';
    +
    +  document.addEventListener('keydown', keyHandler);
    +
    +  function keyHandler(e) {
    +    let isOver = false;
    +    console.log(e.key);
    +
    +    if (e.key === "a") {
    +      result.textContent = 'Player 1 won!!';
    +      isOver = true;
    +    } else if (e.key === "l") {
    +      result.textContent = 'Player 2 won!!';
    +      isOver = true;
    +    }
    +
    +    if (isOver) {
    +      document.removeEventListener('keydown', keyHandler);
    +      setTimeout(reset, 5000);
    +    }
    +  };
    +}
    + +

    Выполните следующие инструкции:

    + +
      +
    1. Во-первых, отмените анимацию спиннера с помощью {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}} (всегда полезно очистить ненужные процессы), и скройте контейнер счётчика.
    2. +
    3. Затем, отобразите абзац с результатами и установите для его текстового содержимого значение "PLAYERS GO!!"  чтобы сообщить игрокам, что теперь они могут нажать свою кнопку, чтобы победить.
    4. +
    5. Прикрепите к документу обработчик событий keydown . При нажатии любой кнопки запускается функция keyHandler().
    6. +
    7. Внутри keyHandler(), код включает объект события в качестве параметра (представленного e) — его свойство {{domxref("KeyboardEvent.key", "key")}} содержит только что нажатую клавишу, и вы можете использовать это для ответа на определённые нажатия клавиш определёнными действиями.
    8. +
    9. Установите для переменной isOver значение false, чтобы мы могли отслеживать, были ли нажаты правильные клавиши, чтобы игрок 1 или 2 выиграл. Мы не хотим, чтобы игра заканчивалась при нажатии неправильной клавиши.
    10. +
    11. Регистрация e.key в консоли, это полезный способ узнать значение различных клавиш, которые вы нажимаете.
    12. +
    13. Когда e.key принимает значение "a", отобразить сообщение о том, что Player 1 выиграл, а когда e.key это "l", отобразить сообщение о том, что Player 2 выиграл. (Note: Это будет работать только со строчными буквами a и l — если переданы прописные A или L , это считается другими клавишами!) Если была нажата одна из этих клавиш, установите для isOver значение true.
    14. +
    15. Только если isOver равно true, удалите обработчик событий keydown с помощью {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} чтобы после того, как произошло выигрышное нажатие, больше не было возможности ввода с клавиатуры, чтобы испортить финальный результат игры. Вы также используете setTimeout() для вызова reset() через 5 секунд — как объяснялось ранее, эта функция сбрасывает игру обратно в исходное состояние, чтобы можно было начать новую игру.
    16. +
    +
  16. +
+ +

Вот и все - вы справились!

+ +
+

Note: Если вы где то застряли, взгляните на наша версия игры (см. также исходный код ).

+
+ +

Заключение

+ +

Вот и все — все основы асинхронных циклов и интервалов рассмотрены в статье. Вы найдёте эти методы полезными во многих ситуациях, но постарайтесь не злоупотреблять ими! Поскольку они по-прежнему выполняются в основном потоке, тяжёлые и интенсивные колбэки (особенно те, которые управляют DOM) могут действительно замедлить страницу, если вы не будете осторожны.

+ +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}

+ +

В этом модуле

+ + + +
+
+
diff --git a/files/ru/conflicting/learn/javascript/asynchronous/introducing/index.html b/files/ru/conflicting/learn/javascript/asynchronous/introducing/index.html new file mode 100644 index 0000000000..1257b751b8 --- /dev/null +++ b/files/ru/conflicting/learn/javascript/asynchronous/introducing/index.html @@ -0,0 +1,165 @@ +--- +title: Основные понятия асинхронного программирования +slug: conflicting/Learn/JavaScript/Asynchronous/Introducing +tags: + - JavaScript + - Promises + - Асинхронность + - Обучение + - блокировки + - потоки +translation_of: Learn/JavaScript/Asynchronous/Concepts +original_slug: Learn/JavaScript/Asynchronous/Concepts +--- +
{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}
+ +

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

+ + + + + + + + + + + + +
Необходимые знания:Базовая компьютерная грамотность, знакомство с основами JavaScript.
Цель:Понять основные идеи асинхронного программирования, и как они проявляются в веб-браузерах и JavaScript.
+ +

Что же такое Асинхронность?

+ +

Как правило, программный код выполняется последовательно, только одна конкретная операция происходит в данный момент времени. Если функция зависит от результата выполнения другой функции, то она должна дождаться пока нужная ей функция не завершит свою работу и не вернёт результат и до тех пор пока это не произойдёт, выполнение программы, по сути, будет остановлено с точки зрения пользователя.

+ +

Пользователь современного ПК, наверняка, наблюдал, как курсор меняет свой вид и становится "разноцветным спинером" (у пользователей MacOS). Таким образом операционная система сообщает - "текущая программа, ожидает завершения какого то длительного процесса в системе и я решила сообщить тебе, что бы ты не волновался".

+ +

Multi-colored macOS beachball busy spinner

+ +

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

+ +

Блокировка кода

+ +

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

+ +

Давайте рассмотрим несколько примеров, которые покажут, что именно значит блокировка.

+ +

В нашем simple-sync.html примере (see it running live), добавим кнопке событие на клик, чтобы при нажатии на неё запускалась трудоёмкая операция (расчёт 10000000 дат, и вывод последней рассчитанной даты на консоль) после чего в DOM добавляется ещё один параграф:

+ +
const btn = document.querySelector('button');
+btn.addEventListener('click', () => {
+  let myDate;
+  for(let i = 0; i < 10000000; i++) {
+    let date = new Date();
+    myDate = date
+  }
+
+  console.log(myDate);
+
+  let pElem = document.createElement('p');
+  pElem.textContent = 'This is a newly-added paragraph.';
+  document.body.appendChild(pElem);
+});
+ +

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

+ +
+

Примечание: Предыдущий пример слишком не реальный. Вам никогда не понадобится считать столько дат в реальном приложении! Однако, он помогает вам понять основную идею.

+
+ +

В нашем следующем примере, simple-sync-ui-blocking.html (посмотреть пример), мы сделаем что-нибудь более реалистичное, с чем вы сможете столкнуться на реальной странице. Мы заблокируем действия пользователя отрисовкой страницы. В этом примере у нас две кнопки:

+ + + +
function expensiveOperation() {
+  for(let i = 0; i < 1000000; i++) {
+    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
+    ctx.beginPath();
+    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
+    ctx.fill()
+  }
+}
+
+fillBtn.addEventListener('click', expensiveOperation);
+
+alertBtn.addEventListener('click', () =>
+  alert('You clicked me!')
+);
+ +

Если вы быстро нажмёте на первую кнопку и затем быстро кликните на вторую, вы увидите, что предупреждение не появится на странице, пока все круги не будут отрисованы. Первая операция блокирует выполнение следующей до тех пор пока не завершится сама.

+ +
+

Примечание: Хорошо, в приведённом некрасивом примере, мы получили эффект блокировки, который показывает общую проблему  при разработке приложений, с которой все время приходится бороться разработчикам.

+
+ +

Почему так происходит? Потому что JavaScript, в общем случае, выполняет команды в одном потоке. Пришло время познакомиться с понятием потока.

+ +

Потоки

+ +

Под потоком, обычно, понимают одиночный процесс, который может использовать программа, для выполнения своих нужд. Каждый поток может выполнять только одну в текущий момент времени:

+ +
Task A --> Task B --> Task C
+ +

Каждая задача будет выполнена последовательно; только когда текущая задача завершится, следующая сможет начаться.

+ +

Как мы говорили выше, большинство компьютеров теперь имеют процессор с несколькими ядрами, т.е. могут выполнять несколько задач одновременно. Языки программирования, поддерживающие многопоточность, могут использовать несколько ядер, чтобы выполнять несколько задач одновременно:

+ +
Thread 1: Task A --> Task B
+Thread 2: Task C --> Task D
+ +

JavaScript однопоточный

+ +

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

+ +
Main thread: Render circles to canvas --> Display alert()
+ +

В итоге, JavaScript получил несколько инструментов, которые могут помочь в решении подобных проблем. Web workers позволяют вам обработать некоторый JavaScript-код в отдельном потоке, который называется обработчик, таким образом вы можете запускать отдельные блоки JavaScript-кода одновременно. В основном, вы будете использовать воркеры, чтобы запустить ресурсоёмкий процесс, отдельно от основного потока, чтобы не блокировать действия пользователя.

+ +
  Main thread: Task A --> Task C
+Worker thread: Expensive task B
+ +

Помня об этом, выполните наш следующий пример simple-sync-worker.html (посмотреть пример в действии), с открытой консолью. Это переписанный предыдущий пример, который теперь рассчитывает 10 миллионов дат в отдельном потоке обработчика. Теперь, когда вы нажимаете на кнопку, браузер может добавить новый элемент на страницу, до того как все даты будут посчитаны. Самая первая операция больше не блокирует выполнение следующей.

+ +

Асинхронный код

+ +

Воркеры полезный инструмент, но у них есть свои ограничения. Самое существенное, заключается в том, что они не имеют доступа к {{Glossary("DOM")}} — вы не можете использовать воркер для обновления UI. Мы не можем отрисовать миллион наших точек  внутри воркера; он может только обработать большой объем информации.

+ +

Следующая проблема заключается в том, что даже если код запущенный в воркере ничего не блокирует, он в целом остаётся синхронным. Это проблема появляется, когда какой-то функции требуются результаты выполнения нескольких предыдущих функций. Рассмотрим следующую диаграмму потоков:

+ +
Main thread: Task A --> Task B
+ +

В этом примере, предположим Task A делает что-то вроде получения картинки с сервера а Task B затем делает что-нибудь с полученной картинкой, например, применяет к ней фильтр. Если запустить выполняться Task A и тут же попытаться выполнить Task B, то вы получите ошибку, поскольку картинка ещё не будет доступна.

+ +
  Main thread: Task A --> Task B --> |Task D|
+Worker thread: Task C -----------> |      |
+ +

Теперь, давайте предположим, что Task D использует результат выполнения обеих задач Task B и Task C. Если мы уверенны, что оба результата будут доступны одновременно, тогда не возникнет проблем, однако, часто это не так. Если Task D попытаться запустить, когда какого-то нужного ей результата ещё нет, выполнение закончится ошибкой.

+ +

Чтобы избежать подобных проблем, браузеры позволяют нам выполнять определённые операции асинхронно. Такие возможности, как Promises позволяют запустить некоторую операцию (например, получение картинки с сервера), и затем подождать пока операция не вернёт результат, перед тем как начать выполнение другой задачи:

+ +
Main thread: Task A                   Task B
+    Promise:      |__async operation__|
+ +

Поскольку операция выполняется где-то отдельно, основной поток не блокируется, при выполнении асинхронных задач.

+ +

В следующей статье, мы покажем вам, как писать асинхронный код. Захватывает дух, неправда ли? Продолжайте читать!

+ +

Заключение

+ +

При проектировании современных программ все больше используется асинхронное программирование, чтобы программа имела возможность выполнять несколько операций в конкретный момент времени. Как только вы начнёте использовать новые, более мощные возможности API, вы обнаружите множество ситуаций, где решить нужную задачу можно только асинхронно. Раньше было сложно писать асинхронный код. До сих пор, нужно время, чтобы привыкнуть к такому подходу, но процесс стал намного легче. Далее, в этом разделе, мы будем глубже исследовать вопрос, когда же асинхронный код необходим и как спроектировать программу, чтобы избежать проблем, описанных выше.

+ +

В этом модуле

+ + diff --git a/files/ru/learn/javascript/asynchronous/async_await/index.html b/files/ru/learn/javascript/asynchronous/async_await/index.html deleted file mode 100644 index e64c9cc30b..0000000000 --- a/files/ru/learn/javascript/asynchronous/async_await/index.html +++ /dev/null @@ -1,415 +0,0 @@ ---- -title: Making asynchronous programming easier with async and await -slug: Learn/JavaScript/Asynchronous/Async_await -tags: - - Асинхронность - - Гайд - - Для новичков -translation_of: Learn/JavaScript/Asynchronous/Async_await ---- -
{{LearnSidebar}}
- -
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}
- -

В ECMAScript версии 2017 появились async functions и ключевое слово await (ECMAScript Next support in Mozilla). По существу, такие функции есть синтаксический сахар над Promises и Generator functions (ts39). С их помощью легче писать/читать асинхронный код, ведь они позволяют использовать привычный синхронный стиль написания. В этой статье мы на базовом уровне разберёмся в их устройстве.

- - - - - - - - - - - - -
Примечания:Чтобы лучше понять материал, желательно перед чтением ознакомиться с основами JavaScript, асинхронными операциями вообще и объектами Promises.
Цель материала:Научить писать современный асинхронный код с использованием Promises и async functions.
- -

Основы async/await

- -

Ключевое слово async

- -

Ключевое слово async позволяет сделать из обычной функции (function declaration или function expression) асинхронную функцию (async function). Такая функция делает две вещи:
- - Оборачивает возвращаемое значение в Promise
- - Позволяет использовать ключевое слово await (см. дальше)

- -

Попробуйте выполнить в консоли браузера следующий код:

- -
function hello() { return "Hello" };
-hello();
- -

Функция возвращает "Hello" — ничего необычного, верно ?

- -

Но что если мы сделаем её асинхронной ? Проверим:

- -
async function hello() { return "Hello" };
-hello();
- -

Как было сказано ранее, вызов асинхронной функции возвращает объект Promise.

- -

Вот пример с async function expression:

- -
let hello = async function() { return "Hello" };
-hello();
- -

Также можно использовать стрелочные функции:

- -
let hello = async () => { return "Hello" };
- -

Все они в общем случае делают одно и то же.

- -

Чтобы получить значение, которое возвращает Promise, мы как обычно можем использовать метод .then():

- -
hello().then((value) => console.log(value))
- -

или ещё короче

- -
hello().then(console.log)
-
- -

Итак, ключевое слово async, превращает обычную функцию в асинхронную и результат вызова функции оборачивает в Promise. Также асинхронная функция позволяет использовать в своём теле ключевое слово await, о котором далее.

- -

Ключевое слово await

- -

Асинхронные функции становятся по настоящему мощными, когда вы используете ключевое слово await  — по факту, await работает только в асинхронных функциях. Мы можем использовать await перед promise-based функцией, чтобы остановить поток выполнения и дождаться результата её выполнения (результат Promise). В то же время, остальной код нашего приложения не блокируется и продолжает работать.

- -

Вы можете использовать await перед любой функцией, что возвращает Promise, включая Browser API функции.

- -

Небольшой пример:

- -
async function hello() {
-  return greeting = await Promise.resolve("Hello");
-};
-
-hello().then(alert);
- -

Конечно, на практике код выше бесполезен, но в учебных целях он иллюстрирует синтаксис асинхронных функций. Теперь давайте перейдём к реальным примерам.

- -

Переписываем Promises с использованием async/await

- -

Давайте посмотрим на пример из предыдущей статьи:

- -
fetch('coffee.jpg')
-.then(response => {
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  } else {
-    return response.blob();
-  }
-})
-.then(myBlob => {
-  let objectURL = URL.createObjectURL(myBlob);
-  let image = document.createElement('img');
-  image.src = objectURL;
-  document.body.appendChild(image);
-})
-.catch(e => {
-  console.log('There has been a problem with your fetch operation: ' + e.message);
-});
- -

К этому моменту вы должны понимать как работают Promises, чтобы понять все остальное. Давайте перепишем код используя async/await и оценим разницу.

- -
async function myFetch() {
-  let response = await fetch('coffee.jpg');
-
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  } else {
-    let myBlob = await response.blob();
-
-    let objectURL = URL.createObjectURL(myBlob);
-    let image = document.createElement('img');
-    image.src = objectURL;
-    document.body.appendChild(image);
-  }
-}
-
-myFetch()
-.catch(e => {
-  console.log('There has been a problem with your fetch operation: ' + e.message);
-});
- -

Согласитесь, что код стал короче и понятнее — больше никаких блоков .then() по всему скрипту!

- -

Так как ключевое слово async заставляет функцию вернуть Promise, мы можем использовать гибридный подход:

- -
async function myFetch() {
-  let response = await fetch('coffee.jpg');
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  } else {
-    return await response.blob();
-  }
-}
-
-myFetch().then((blob) => {
-  let objectURL = URL.createObjectURL(blob);
-  let image = document.createElement('img');
-  image.src = objectURL;
-  document.body.appendChild(image);
-}).catch(e => console.log(e));
- -

Можете попрактиковаться самостоятельно, или запустить наш live example (а также source code).

- -

Минуточку, а как это все работает ?

- -

Вы могли заметить, что мы обернули наш код в функцию и сделали её асинхронной с помощью async. Это было обязательно - нам надо создать контейнер, внутри которого будет запускаться асинхронный код и будет возможность дождаться его результата с помощью await, не блокируя остальной код нашего скрипта.

- -

Внутри myFetch() находится код, который слегка напоминает версию на Promise, но есть важные отличия. Вместо того, чтобы писать цепочку блоков .then() мы просто использует ключевое слово await перед вызовом promise-based функции и присваиваем результат в переменную. Ключевое слово await говорит JavaScript runtime приостановить код в этой строке, не блокируя остальной код скрипта за пределами асинхронной функции. Когда вызов promise-based функции будет готов вернуть результат, выполнение продолжится с этой строки дальше.
-
- Пример:

- -
let response = await fetch('coffee.jpg');
- -

Значение Promise, которое вернёт fetch() будет присвоено переменной response только тогда, когда оно будет доступно - парсер делает паузу на данной строке дожидаясь этого момента. Как только значение доступно, парсер переходит к следующей строке, в которой создаётся объект Blob из результата Promise. В этой строке, кстати, также используется await, потому что метод .blob() также возвращает Promise. Когда результат готов, мы возвращаем его наружу из myFetch().

- -

Обратите внимание, когда мы вызываем myFetch(), она возвращает Promise, поэтому мы можем вызвать .then() на результате, чтобы отобразить его на экране.
-
- К этому моменту вы наверное думаете "Это реально круто!", и вы правы - чем меньше блоков .then(), тем легче читать код.

- -

Добавляем обработку ошибок

- -


- Чтобы обработать ошибки у нас есть несколько вариантов

- -

Мы можем использовать синхронную try...catch структуру с async/await. Вот изменённая версия первого примера выше:

- -
async function myFetch() {
-  try {
-    let response = await fetch('coffee.jpg');
-
-    if (!response.ok) {
-      throw new Error(`HTTP error! status: ${response.status}`);
-    } else {
-      let myBlob = await response.blob();
-      let objectURL = URL.createObjectURL(myBlob);
-      let image = document.createElement('img');
-      image.src = objectURL;
-      document.body.appendChild(image);
-    }
-  } catch(e) {
-    console.log(e);
-  }
-}
-
-myFetch();
- -

В блок catch() {} передаётся объект ошибки, который мы назвали e; мы можем вывести его в консоль, чтобы посмотреть детали: где и почему возникла ошибка.

- -

Если вы хотите использовать гибридный подходы (пример выше), лучше использовать блок .catch() после блока .then() вот так:

- -
async function myFetch() {
-  let response = await fetch('coffee.jpg');
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  } else {
-    return await response.blob();
-  }
-}
-
-myFetch().then((blob) => {
-  let objectURL = URL.createObjectURL(blob);
-  let image = document.createElement('img');
-  image.src = objectURL;
-  document.body.appendChild(image);
-})
-.catch((e) =>
-  console.log(e)
-);
- -

Так лучше, потому что блок .catch() словит ошибки как из асинхронной функции, так и из Promise. Если бы мы использовали блок try/catch, мы бы не словили ошибку, которая произошла в самой myFetch() функции.

- -

Вы можете посмотреть оба примера на GitHub:

- - - -

Await и Promise.all()

- -

Как вы помните, асинхронные функции построены поверх promises, поэтому они совместимы со всеми возможностями последних. Мы легко можем подождать выполнение Promise.all(), присвоить результат в переменную и все это сделать используя синхронный стиль. Опять, вернёмся к примеру, рассмотренному в предыдущей статье. Откройте пример в соседней вкладке, чтобы лучше понять разницу.

- -

Версия с async/await (смотрите live demo и source code), сейчас выглядит так:

- -
async function fetchAndDecode(url, type) {
-  let response = await fetch(url);
-
-  let content;
-
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  } else {
-    if(type === 'blob') {
-      content = await response.blob();
-    } else if(type === 'text') {
-      content = await response.text();
-    }
-
-    return content;
-  }
-
-}
-
-async function displayContent() {
-  let coffee = fetchAndDecode('coffee.jpg', 'blob');
-  let tea = fetchAndDecode('tea.jpg', 'blob');
-  let description = fetchAndDecode('description.txt', 'text');
-
-  let values = await Promise.all([coffee, tea, description]);
-
-  let objectURL1 = URL.createObjectURL(values[0]);
-  let objectURL2 = URL.createObjectURL(values[1]);
-  let descText = values[2];
-
-  let image1 = document.createElement('img');
-  let image2 = document.createElement('img');
-  image1.src = objectURL1;
-  image2.src = objectURL2;
-  document.body.appendChild(image1);
-  document.body.appendChild(image2);
-
-  let para = document.createElement('p');
-  para.textContent = descText;
-  document.body.appendChild(para);
-}
-
-displayContent()
-.catch((e) =>
-  console.log(e)
-);
- -

Вы видите, что мы легко изменили fetchAndDecode() функцию в асинхронный вариант. Взгляните на строку с Promise.all():

- -
let values = await Promise.all([coffee, tea, description]);
- -

С помощью await мы ждём массив результатов всех трёх Promises и присваиваем его в переменную values. Это асинхронный код, но он написан в синхронном стиле, за счёт чего он гораздо читабельнее.
-
- Мы должны обернуть весь код в синхронную функцию, displayContent(), и мы не сильно сэкономили на количестве кода, но мы извлекли код блока .then(), за счёт чего наш код стал гораздо чище.

- -

Для обработки ошибок мы добавили блок .catch() для функции displayContent(); Это позволило нам отловить ошибки в обоих функциях.

- -
-

Примечание: Мы также можем использовать синхронный блок finally внутри асинхронной функции, вместо асинхронного .finally(), чтобы получить информацию о результате нашей операции — смотрите в действии в нашем live example (смотрите source code).

-
- -

Недостатки async/await

- -

Асинхронные функции с async/await бывают очень удобными, но есть несколько замечаний, о которых полезно знать.

- -

Async/await позволяет вам писать код в синхронном стиле. Ключевое слово await блокирует приостанавливает выполнение ptomise-based функции до того момента, пока promise примет статус fulfilled. Это не блокирует код за пределами вашей асинхронной функции, тем не менее важно помнить, что внутри асинхронной функции поток выполнения блокируется.
-
- ваш код может стать медленнее за счёт большого количества awaited promises, которые идут один за другим. Каждый await должен дождаться выполнения предыдущего, тогда как на самом деле мы хотим, чтобы наши Promises выполнялись одновременно, как если бы мы не использовали async/await.
-
- Есть подход, который позволяет обойти эту проблему - сохранить все выполняющиеся Promises в переменные, а уже после этого дожидаться (awaiting) их результата. Давайте посмотрим на несколько примеров.

- -

Мы подготовили два примера  — slow-async-await.html (см. source code) и fast-async-await.html (см. source code). Они оба начинаются с функции возвращающей promise, имитирующей асинхронность процессов при помощи вызова setTimeout():

- -
function timeoutPromise(interval) {
-  return new Promise((resolve, reject) => {
-    setTimeout(function(){
-      resolve("done");
-    }, interval);
-  });
-};
- -

Далее в каждом примере есть асинхронная функция  timeTest() ожидающая три вызова timeoutPromise():

- -
async function timeTest() {
-  ...
-}
- -

В каждом примере функция записывает время начала исполнения и сколько времени понадобилось на исполнение  timeTest()  промисов, вычитая время в момент запуска функции из времени в момент разрешения промисов:

- -
let startTime = Date.now();
-timeTest().then(() => {
-  let finishTime = Date.now();
-  let timeTaken = finishTime - startTime;
-  alert("Time taken in milliseconds: " + timeTaken);
-})
- -

Далее представлена асинхронная функция timeTest() различная для каждого из примеров.

- -

В случае с медленным примером slow-async-await.html, timeTest() выглядит:

- -
async function timeTest() {
-  await timeoutPromise(3000);
-  await timeoutPromise(3000);
-  await timeoutPromise(3000);
-}
- -

Здесь мы просто ждём все три  timeoutPromise() напрямую, блокируя выполнение на данного блока на 3 секунды при каждом вызове. Все последующие вызовы вынуждены ждать пока разрешится предыдущий. Если вы запустите первый пример (slow-async-await.html) вы увидите alert сообщающий время выполнения около 9 секунд. 

- -

Во втором  fast-async-await.html примере, функция timeTest() выглядит как:

- -
async function timeTest() {
-  const timeoutPromise1 = timeoutPromise(3000);
-  const timeoutPromise2 = timeoutPromise(3000);
-  const timeoutPromise3 = timeoutPromise(3000);
-
-  await timeoutPromise1;
-  await timeoutPromise2;
-  await timeoutPromise3;
-}
- -

В данном случае мы храним три объекта Promise в переменных,  каждый из которых может разрешиться независимо от других.

- -

Ниже мы ожидаем разрешения промисов из объекта в результат, так как они были запущенны одновременно, блокируя поток, то и разрешатся одновременно. Если вы запустите второй пример вы увидите alert, сообщающий время выполнения около 3 секунд.

- -

Важно не забывать о быстродействии применяя await, проверяйте количество блокировок.

- -

-

Async/await class methods

- - -

В качестве последнего замечания, вы можете использовать  async  перед методами классов или объектов, вынуждая их возвращать promises. А также  await внутри методов объявленных таким образом. Посмотрите на пример ES class code, который мы наблюдали в статье  object-oriented JavaScript,  и сравните его с модифицированной (асинхронной) async версией ниже:

- -
class Person {
-  constructor(first, last, age, gender, interests) {
-    this.name = {
-      first,
-      last
-    };
-    this.age = age;
-    this.gender = gender;
-    this.interests = interests;
-  }
-
-  async greeting() {
-    return await Promise.resolve(`Hi! I'm ${this.name.first}`);
-  };
-
-  farewell() {
-    console.log(`${this.name.first} has left the building. Bye for now!`);
-  };
-}
-
-let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
- -

Первый метод класса теперь можно использовать таким образом:

- -
han.greeting().then(console.log);
- -

Browser support (Поддержка браузерами)

- -

One consideration when deciding whether to use async/await is support for older browsers. They are available in modern versions of most browsers, the same as promises; the main support problems come with Internet Explorer and Opera Mini.

- -

If you want to use async/await but are concerned about older browser support, you could consider using the BabelJS library — this allows you to write your applications using the latest JavaScript and let Babel figure out what changes if any are needed for your user’s browsers. On encountering a browser that does not support async/await, Babel's polyfill can automatically provide fallbacks that work in older browsers.

- -

Заключение

- -

Вот пожалуй и все - async/await позволяют писать асинхронный код, который легче читать и поддерживать. Даже учитывая, что поддержка со стороны браузеров несколько хуже, чем у promise.then, всё же стоит обратить на него внимание.

- -

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}

- -

In this module

- - diff --git a/files/ru/learn/javascript/asynchronous/concepts/index.html b/files/ru/learn/javascript/asynchronous/concepts/index.html deleted file mode 100644 index fe4e6c7343..0000000000 --- a/files/ru/learn/javascript/asynchronous/concepts/index.html +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Основные понятия асинхронного программирования -slug: Learn/JavaScript/Asynchronous/Concepts -tags: - - JavaScript - - Promises - - Асинхронность - - Обучение - - блокировки - - потоки -translation_of: Learn/JavaScript/Asynchronous/Concepts ---- -
{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}
- -

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

- - - - - - - - - - - - -
Необходимые знания:Базовая компьютерная грамотность, знакомство с основами JavaScript.
Цель:Понять основные идеи асинхронного программирования, и как они проявляются в веб-браузерах и JavaScript.
- -

Что же такое Асинхронность?

- -

Как правило, программный код выполняется последовательно, только одна конкретная операция происходит в данный момент времени. Если функция зависит от результата выполнения другой функции, то она должна дождаться пока нужная ей функция не завершит свою работу и не вернёт результат и до тех пор пока это не произойдёт, выполнение программы, по сути, будет остановлено с точки зрения пользователя.

- -

Пользователь современного ПК, наверняка, наблюдал, как курсор меняет свой вид и становится "разноцветным спинером" (у пользователей MacOS). Таким образом операционная система сообщает - "текущая программа, ожидает завершения какого то длительного процесса в системе и я решила сообщить тебе, что бы ты не волновался".

- -

Multi-colored macOS beachball busy spinner

- -

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

- -

Блокировка кода

- -

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

- -

Давайте рассмотрим несколько примеров, которые покажут, что именно значит блокировка.

- -

В нашем simple-sync.html примере (see it running live), добавим кнопке событие на клик, чтобы при нажатии на неё запускалась трудоёмкая операция (расчёт 10000000 дат, и вывод последней рассчитанной даты на консоль) после чего в DOM добавляется ещё один параграф:

- -
const btn = document.querySelector('button');
-btn.addEventListener('click', () => {
-  let myDate;
-  for(let i = 0; i < 10000000; i++) {
-    let date = new Date();
-    myDate = date
-  }
-
-  console.log(myDate);
-
-  let pElem = document.createElement('p');
-  pElem.textContent = 'This is a newly-added paragraph.';
-  document.body.appendChild(pElem);
-});
- -

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

- -
-

Примечание: Предыдущий пример слишком не реальный. Вам никогда не понадобится считать столько дат в реальном приложении! Однако, он помогает вам понять основную идею.

-
- -

В нашем следующем примере, simple-sync-ui-blocking.html (посмотреть пример), мы сделаем что-нибудь более реалистичное, с чем вы сможете столкнуться на реальной странице. Мы заблокируем действия пользователя отрисовкой страницы. В этом примере у нас две кнопки:

- - - -
function expensiveOperation() {
-  for(let i = 0; i < 1000000; i++) {
-    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
-    ctx.beginPath();
-    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
-    ctx.fill()
-  }
-}
-
-fillBtn.addEventListener('click', expensiveOperation);
-
-alertBtn.addEventListener('click', () =>
-  alert('You clicked me!')
-);
- -

Если вы быстро нажмёте на первую кнопку и затем быстро кликните на вторую, вы увидите, что предупреждение не появится на странице, пока все круги не будут отрисованы. Первая операция блокирует выполнение следующей до тех пор пока не завершится сама.

- -
-

Примечание: Хорошо, в приведённом некрасивом примере, мы получили эффект блокировки, который показывает общую проблему  при разработке приложений, с которой все время приходится бороться разработчикам.

-
- -

Почему так происходит? Потому что JavaScript, в общем случае, выполняет команды в одном потоке. Пришло время познакомиться с понятием потока.

- -

Потоки

- -

Под потоком, обычно, понимают одиночный процесс, который может использовать программа, для выполнения своих нужд. Каждый поток может выполнять только одну в текущий момент времени:

- -
Task A --> Task B --> Task C
- -

Каждая задача будет выполнена последовательно; только когда текущая задача завершится, следующая сможет начаться.

- -

Как мы говорили выше, большинство компьютеров теперь имеют процессор с несколькими ядрами, т.е. могут выполнять несколько задач одновременно. Языки программирования, поддерживающие многопоточность, могут использовать несколько ядер, чтобы выполнять несколько задач одновременно:

- -
Thread 1: Task A --> Task B
-Thread 2: Task C --> Task D
- -

JavaScript однопоточный

- -

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

- -
Main thread: Render circles to canvas --> Display alert()
- -

В итоге, JavaScript получил несколько инструментов, которые могут помочь в решении подобных проблем. Web workers позволяют вам обработать некоторый JavaScript-код в отдельном потоке, который называется обработчик, таким образом вы можете запускать отдельные блоки JavaScript-кода одновременно. В основном, вы будете использовать воркеры, чтобы запустить ресурсоёмкий процесс, отдельно от основного потока, чтобы не блокировать действия пользователя.

- -
  Main thread: Task A --> Task C
-Worker thread: Expensive task B
- -

Помня об этом, выполните наш следующий пример simple-sync-worker.html (посмотреть пример в действии), с открытой консолью. Это переписанный предыдущий пример, который теперь рассчитывает 10 миллионов дат в отдельном потоке обработчика. Теперь, когда вы нажимаете на кнопку, браузер может добавить новый элемент на страницу, до того как все даты будут посчитаны. Самая первая операция больше не блокирует выполнение следующей.

- -

Асинхронный код

- -

Воркеры полезный инструмент, но у них есть свои ограничения. Самое существенное, заключается в том, что они не имеют доступа к {{Glossary("DOM")}} — вы не можете использовать воркер для обновления UI. Мы не можем отрисовать миллион наших точек  внутри воркера; он может только обработать большой объем информации.

- -

Следующая проблема заключается в том, что даже если код запущенный в воркере ничего не блокирует, он в целом остаётся синхронным. Это проблема появляется, когда какой-то функции требуются результаты выполнения нескольких предыдущих функций. Рассмотрим следующую диаграмму потоков:

- -
Main thread: Task A --> Task B
- -

В этом примере, предположим Task A делает что-то вроде получения картинки с сервера а Task B затем делает что-нибудь с полученной картинкой, например, применяет к ней фильтр. Если запустить выполняться Task A и тут же попытаться выполнить Task B, то вы получите ошибку, поскольку картинка ещё не будет доступна.

- -
  Main thread: Task A --> Task B --> |Task D|
-Worker thread: Task C -----------> |      |
- -

Теперь, давайте предположим, что Task D использует результат выполнения обеих задач Task B и Task C. Если мы уверенны, что оба результата будут доступны одновременно, тогда не возникнет проблем, однако, часто это не так. Если Task D попытаться запустить, когда какого-то нужного ей результата ещё нет, выполнение закончится ошибкой.

- -

Чтобы избежать подобных проблем, браузеры позволяют нам выполнять определённые операции асинхронно. Такие возможности, как Promises позволяют запустить некоторую операцию (например, получение картинки с сервера), и затем подождать пока операция не вернёт результат, перед тем как начать выполнение другой задачи:

- -
Main thread: Task A                   Task B
-    Promise:      |__async operation__|
- -

Поскольку операция выполняется где-то отдельно, основной поток не блокируется, при выполнении асинхронных задач.

- -

В следующей статье, мы покажем вам, как писать асинхронный код. Захватывает дух, неправда ли? Продолжайте читать!

- -

Заключение

- -

При проектировании современных программ все больше используется асинхронное программирование, чтобы программа имела возможность выполнять несколько операций в конкретный момент времени. Как только вы начнёте использовать новые, более мощные возможности API, вы обнаружите множество ситуаций, где решить нужную задачу можно только асинхронно. Раньше было сложно писать асинхронный код. До сих пор, нужно время, чтобы привыкнуть к такому подходу, но процесс стал намного легче. Далее, в этом разделе, мы будем глубже исследовать вопрос, когда же асинхронный код необходим и как спроектировать программу, чтобы избежать проблем, описанных выше.

- -

В этом модуле

- - diff --git a/files/ru/learn/javascript/asynchronous/promises/index.html b/files/ru/learn/javascript/asynchronous/promises/index.html new file mode 100644 index 0000000000..4bf8481105 --- /dev/null +++ b/files/ru/learn/javascript/asynchronous/promises/index.html @@ -0,0 +1,416 @@ +--- +title: Making asynchronous programming easier with async and await +slug: Learn/JavaScript/Asynchronous/Promises +tags: + - Асинхронность + - Гайд + - Для новичков +translation_of: Learn/JavaScript/Asynchronous/Async_await +original_slug: Learn/JavaScript/Asynchronous/Async_await +--- +
{{LearnSidebar}}
+ +
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}
+ +

В ECMAScript версии 2017 появились async functions и ключевое слово await (ECMAScript Next support in Mozilla). По существу, такие функции есть синтаксический сахар над Promises и Generator functions (ts39). С их помощью легче писать/читать асинхронный код, ведь они позволяют использовать привычный синхронный стиль написания. В этой статье мы на базовом уровне разберёмся в их устройстве.

+ + + + + + + + + + + + +
Примечания:Чтобы лучше понять материал, желательно перед чтением ознакомиться с основами JavaScript, асинхронными операциями вообще и объектами Promises.
Цель материала:Научить писать современный асинхронный код с использованием Promises и async functions.
+ +

Основы async/await

+ +

Ключевое слово async

+ +

Ключевое слово async позволяет сделать из обычной функции (function declaration или function expression) асинхронную функцию (async function). Такая функция делает две вещи:
+ - Оборачивает возвращаемое значение в Promise
+ - Позволяет использовать ключевое слово await (см. дальше)

+ +

Попробуйте выполнить в консоли браузера следующий код:

+ +
function hello() { return "Hello" };
+hello();
+ +

Функция возвращает "Hello" — ничего необычного, верно ?

+ +

Но что если мы сделаем её асинхронной ? Проверим:

+ +
async function hello() { return "Hello" };
+hello();
+ +

Как было сказано ранее, вызов асинхронной функции возвращает объект Promise.

+ +

Вот пример с async function expression:

+ +
let hello = async function() { return "Hello" };
+hello();
+ +

Также можно использовать стрелочные функции:

+ +
let hello = async () => { return "Hello" };
+ +

Все они в общем случае делают одно и то же.

+ +

Чтобы получить значение, которое возвращает Promise, мы как обычно можем использовать метод .then():

+ +
hello().then((value) => console.log(value))
+ +

или ещё короче

+ +
hello().then(console.log)
+
+ +

Итак, ключевое слово async, превращает обычную функцию в асинхронную и результат вызова функции оборачивает в Promise. Также асинхронная функция позволяет использовать в своём теле ключевое слово await, о котором далее.

+ +

Ключевое слово await

+ +

Асинхронные функции становятся по настоящему мощными, когда вы используете ключевое слово await  — по факту, await работает только в асинхронных функциях. Мы можем использовать await перед promise-based функцией, чтобы остановить поток выполнения и дождаться результата её выполнения (результат Promise). В то же время, остальной код нашего приложения не блокируется и продолжает работать.

+ +

Вы можете использовать await перед любой функцией, что возвращает Promise, включая Browser API функции.

+ +

Небольшой пример:

+ +
async function hello() {
+  return greeting = await Promise.resolve("Hello");
+};
+
+hello().then(alert);
+ +

Конечно, на практике код выше бесполезен, но в учебных целях он иллюстрирует синтаксис асинхронных функций. Теперь давайте перейдём к реальным примерам.

+ +

Переписываем Promises с использованием async/await

+ +

Давайте посмотрим на пример из предыдущей статьи:

+ +
fetch('coffee.jpg')
+.then(response => {
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    return response.blob();
+  }
+})
+.then(myBlob => {
+  let objectURL = URL.createObjectURL(myBlob);
+  let image = document.createElement('img');
+  image.src = objectURL;
+  document.body.appendChild(image);
+})
+.catch(e => {
+  console.log('There has been a problem with your fetch operation: ' + e.message);
+});
+ +

К этому моменту вы должны понимать как работают Promises, чтобы понять все остальное. Давайте перепишем код используя async/await и оценим разницу.

+ +
async function myFetch() {
+  let response = await fetch('coffee.jpg');
+
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    let myBlob = await response.blob();
+
+    let objectURL = URL.createObjectURL(myBlob);
+    let image = document.createElement('img');
+    image.src = objectURL;
+    document.body.appendChild(image);
+  }
+}
+
+myFetch()
+.catch(e => {
+  console.log('There has been a problem with your fetch operation: ' + e.message);
+});
+ +

Согласитесь, что код стал короче и понятнее — больше никаких блоков .then() по всему скрипту!

+ +

Так как ключевое слово async заставляет функцию вернуть Promise, мы можем использовать гибридный подход:

+ +
async function myFetch() {
+  let response = await fetch('coffee.jpg');
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    return await response.blob();
+  }
+}
+
+myFetch().then((blob) => {
+  let objectURL = URL.createObjectURL(blob);
+  let image = document.createElement('img');
+  image.src = objectURL;
+  document.body.appendChild(image);
+}).catch(e => console.log(e));
+ +

Можете попрактиковаться самостоятельно, или запустить наш live example (а также source code).

+ +

Минуточку, а как это все работает ?

+ +

Вы могли заметить, что мы обернули наш код в функцию и сделали её асинхронной с помощью async. Это было обязательно - нам надо создать контейнер, внутри которого будет запускаться асинхронный код и будет возможность дождаться его результата с помощью await, не блокируя остальной код нашего скрипта.

+ +

Внутри myFetch() находится код, который слегка напоминает версию на Promise, но есть важные отличия. Вместо того, чтобы писать цепочку блоков .then() мы просто использует ключевое слово await перед вызовом promise-based функции и присваиваем результат в переменную. Ключевое слово await говорит JavaScript runtime приостановить код в этой строке, не блокируя остальной код скрипта за пределами асинхронной функции. Когда вызов promise-based функции будет готов вернуть результат, выполнение продолжится с этой строки дальше.
+
+ Пример:

+ +
let response = await fetch('coffee.jpg');
+ +

Значение Promise, которое вернёт fetch() будет присвоено переменной response только тогда, когда оно будет доступно - парсер делает паузу на данной строке дожидаясь этого момента. Как только значение доступно, парсер переходит к следующей строке, в которой создаётся объект Blob из результата Promise. В этой строке, кстати, также используется await, потому что метод .blob() также возвращает Promise. Когда результат готов, мы возвращаем его наружу из myFetch().

+ +

Обратите внимание, когда мы вызываем myFetch(), она возвращает Promise, поэтому мы можем вызвать .then() на результате, чтобы отобразить его на экране.
+
+ К этому моменту вы наверное думаете "Это реально круто!", и вы правы - чем меньше блоков .then(), тем легче читать код.

+ +

Добавляем обработку ошибок

+ +


+ Чтобы обработать ошибки у нас есть несколько вариантов

+ +

Мы можем использовать синхронную try...catch структуру с async/await. Вот изменённая версия первого примера выше:

+ +
async function myFetch() {
+  try {
+    let response = await fetch('coffee.jpg');
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    } else {
+      let myBlob = await response.blob();
+      let objectURL = URL.createObjectURL(myBlob);
+      let image = document.createElement('img');
+      image.src = objectURL;
+      document.body.appendChild(image);
+    }
+  } catch(e) {
+    console.log(e);
+  }
+}
+
+myFetch();
+ +

В блок catch() {} передаётся объект ошибки, который мы назвали e; мы можем вывести его в консоль, чтобы посмотреть детали: где и почему возникла ошибка.

+ +

Если вы хотите использовать гибридный подходы (пример выше), лучше использовать блок .catch() после блока .then() вот так:

+ +
async function myFetch() {
+  let response = await fetch('coffee.jpg');
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    return await response.blob();
+  }
+}
+
+myFetch().then((blob) => {
+  let objectURL = URL.createObjectURL(blob);
+  let image = document.createElement('img');
+  image.src = objectURL;
+  document.body.appendChild(image);
+})
+.catch((e) =>
+  console.log(e)
+);
+ +

Так лучше, потому что блок .catch() словит ошибки как из асинхронной функции, так и из Promise. Если бы мы использовали блок try/catch, мы бы не словили ошибку, которая произошла в самой myFetch() функции.

+ +

Вы можете посмотреть оба примера на GitHub:

+ + + +

Await и Promise.all()

+ +

Как вы помните, асинхронные функции построены поверх promises, поэтому они совместимы со всеми возможностями последних. Мы легко можем подождать выполнение Promise.all(), присвоить результат в переменную и все это сделать используя синхронный стиль. Опять, вернёмся к примеру, рассмотренному в предыдущей статье. Откройте пример в соседней вкладке, чтобы лучше понять разницу.

+ +

Версия с async/await (смотрите live demo и source code), сейчас выглядит так:

+ +
async function fetchAndDecode(url, type) {
+  let response = await fetch(url);
+
+  let content;
+
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  } else {
+    if(type === 'blob') {
+      content = await response.blob();
+    } else if(type === 'text') {
+      content = await response.text();
+    }
+
+    return content;
+  }
+
+}
+
+async function displayContent() {
+  let coffee = fetchAndDecode('coffee.jpg', 'blob');
+  let tea = fetchAndDecode('tea.jpg', 'blob');
+  let description = fetchAndDecode('description.txt', 'text');
+
+  let values = await Promise.all([coffee, tea, description]);
+
+  let objectURL1 = URL.createObjectURL(values[0]);
+  let objectURL2 = URL.createObjectURL(values[1]);
+  let descText = values[2];
+
+  let image1 = document.createElement('img');
+  let image2 = document.createElement('img');
+  image1.src = objectURL1;
+  image2.src = objectURL2;
+  document.body.appendChild(image1);
+  document.body.appendChild(image2);
+
+  let para = document.createElement('p');
+  para.textContent = descText;
+  document.body.appendChild(para);
+}
+
+displayContent()
+.catch((e) =>
+  console.log(e)
+);
+ +

Вы видите, что мы легко изменили fetchAndDecode() функцию в асинхронный вариант. Взгляните на строку с Promise.all():

+ +
let values = await Promise.all([coffee, tea, description]);
+ +

С помощью await мы ждём массив результатов всех трёх Promises и присваиваем его в переменную values. Это асинхронный код, но он написан в синхронном стиле, за счёт чего он гораздо читабельнее.
+
+ Мы должны обернуть весь код в синхронную функцию, displayContent(), и мы не сильно сэкономили на количестве кода, но мы извлекли код блока .then(), за счёт чего наш код стал гораздо чище.

+ +

Для обработки ошибок мы добавили блок .catch() для функции displayContent(); Это позволило нам отловить ошибки в обоих функциях.

+ +
+

Примечание: Мы также можем использовать синхронный блок finally внутри асинхронной функции, вместо асинхронного .finally(), чтобы получить информацию о результате нашей операции — смотрите в действии в нашем live example (смотрите source code).

+
+ +

Недостатки async/await

+ +

Асинхронные функции с async/await бывают очень удобными, но есть несколько замечаний, о которых полезно знать.

+ +

Async/await позволяет вам писать код в синхронном стиле. Ключевое слово await блокирует приостанавливает выполнение ptomise-based функции до того момента, пока promise примет статус fulfilled. Это не блокирует код за пределами вашей асинхронной функции, тем не менее важно помнить, что внутри асинхронной функции поток выполнения блокируется.
+
+ ваш код может стать медленнее за счёт большого количества awaited promises, которые идут один за другим. Каждый await должен дождаться выполнения предыдущего, тогда как на самом деле мы хотим, чтобы наши Promises выполнялись одновременно, как если бы мы не использовали async/await.
+
+ Есть подход, который позволяет обойти эту проблему - сохранить все выполняющиеся Promises в переменные, а уже после этого дожидаться (awaiting) их результата. Давайте посмотрим на несколько примеров.

+ +

Мы подготовили два примера  — slow-async-await.html (см. source code) и fast-async-await.html (см. source code). Они оба начинаются с функции возвращающей promise, имитирующей асинхронность процессов при помощи вызова setTimeout():

+ +
function timeoutPromise(interval) {
+  return new Promise((resolve, reject) => {
+    setTimeout(function(){
+      resolve("done");
+    }, interval);
+  });
+};
+ +

Далее в каждом примере есть асинхронная функция  timeTest() ожидающая три вызова timeoutPromise():

+ +
async function timeTest() {
+  ...
+}
+ +

В каждом примере функция записывает время начала исполнения и сколько времени понадобилось на исполнение  timeTest()  промисов, вычитая время в момент запуска функции из времени в момент разрешения промисов:

+ +
let startTime = Date.now();
+timeTest().then(() => {
+  let finishTime = Date.now();
+  let timeTaken = finishTime - startTime;
+  alert("Time taken in milliseconds: " + timeTaken);
+})
+ +

Далее представлена асинхронная функция timeTest() различная для каждого из примеров.

+ +

В случае с медленным примером slow-async-await.html, timeTest() выглядит:

+ +
async function timeTest() {
+  await timeoutPromise(3000);
+  await timeoutPromise(3000);
+  await timeoutPromise(3000);
+}
+ +

Здесь мы просто ждём все три  timeoutPromise() напрямую, блокируя выполнение на данного блока на 3 секунды при каждом вызове. Все последующие вызовы вынуждены ждать пока разрешится предыдущий. Если вы запустите первый пример (slow-async-await.html) вы увидите alert сообщающий время выполнения около 9 секунд. 

+ +

Во втором  fast-async-await.html примере, функция timeTest() выглядит как:

+ +
async function timeTest() {
+  const timeoutPromise1 = timeoutPromise(3000);
+  const timeoutPromise2 = timeoutPromise(3000);
+  const timeoutPromise3 = timeoutPromise(3000);
+
+  await timeoutPromise1;
+  await timeoutPromise2;
+  await timeoutPromise3;
+}
+ +

В данном случае мы храним три объекта Promise в переменных,  каждый из которых может разрешиться независимо от других.

+ +

Ниже мы ожидаем разрешения промисов из объекта в результат, так как они были запущенны одновременно, блокируя поток, то и разрешатся одновременно. Если вы запустите второй пример вы увидите alert, сообщающий время выполнения около 3 секунд.

+ +

Важно не забывать о быстродействии применяя await, проверяйте количество блокировок.

+ +

+

Async/await class methods

+ + +

В качестве последнего замечания, вы можете использовать  async  перед методами классов или объектов, вынуждая их возвращать promises. А также  await внутри методов объявленных таким образом. Посмотрите на пример ES class code, который мы наблюдали в статье  object-oriented JavaScript,  и сравните его с модифицированной (асинхронной) async версией ниже:

+ +
class Person {
+  constructor(first, last, age, gender, interests) {
+    this.name = {
+      first,
+      last
+    };
+    this.age = age;
+    this.gender = gender;
+    this.interests = interests;
+  }
+
+  async greeting() {
+    return await Promise.resolve(`Hi! I'm ${this.name.first}`);
+  };
+
+  farewell() {
+    console.log(`${this.name.first} has left the building. Bye for now!`);
+  };
+}
+
+let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
+ +

Первый метод класса теперь можно использовать таким образом:

+ +
han.greeting().then(console.log);
+ +

Browser support (Поддержка браузерами)

+ +

One consideration when deciding whether to use async/await is support for older browsers. They are available in modern versions of most browsers, the same as promises; the main support problems come with Internet Explorer and Opera Mini.

+ +

If you want to use async/await but are concerned about older browser support, you could consider using the BabelJS library — this allows you to write your applications using the latest JavaScript and let Babel figure out what changes if any are needed for your user’s browsers. On encountering a browser that does not support async/await, Babel's polyfill can automatically provide fallbacks that work in older browsers.

+ +

Заключение

+ +

Вот пожалуй и все - async/await позволяют писать асинхронный код, который легче читать и поддерживать. Даже учитывая, что поддержка со стороны браузеров несколько хуже, чем у promise.then, всё же стоит обратить на него внимание.

+ +

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}

+ +

In this module

+ + diff --git a/files/ru/learn/javascript/asynchronous/timeouts_and_intervals/index.html b/files/ru/learn/javascript/asynchronous/timeouts_and_intervals/index.html deleted file mode 100644 index 19019a19a9..0000000000 --- a/files/ru/learn/javascript/asynchronous/timeouts_and_intervals/index.html +++ /dev/null @@ -1,639 +0,0 @@ ---- -title: 'Объединённый асинхронный JavaScript: Таймауты и интервалы' -slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals -translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals -original_slug: Learn/JavaScript/Asynchronous/Таймауты_и_интервалы ---- -
{{LearnSidebar}}
- -
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}
- -

В этом руководстве рассматриваются традиционные методы, доступные в JavaScript для асинхронного выполнения кода по истечении заданного периода времени или через регулярный интервал (например, заданное количество раз в секунду), обсуждаются их полезные свойства и рассматриваются присущие им проблемы. .

- - - - - - - - - - - - -
Необходимые условия:Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель:Понимание асинхронных циклов и интервалов, и то как их можно использовать.
- -

Введение

- -

В течение долгого времени веб-платформа предлагала программистам JavaScript ряд функций, которые позволяли им асинхронно выполнять код по истечении определённого временного интервала и повторно выполнять асинхронный блок кода, пока вы не скажете ему остановиться.

- -

Эти функции:

- -
-
setTimeout()
-
Выполняет указанный блок кода один раз по истечении указанного времени
-
setInterval()
-
Выполняет указанный блок кода несколько раз с определённым интервалом между каждым вызовом.
-
requestAnimationFrame()
-
Современная версия setInterval (). Выполняют указанный блок кода перед тем, как браузер в следующий раз перерисовывает отображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.
-
- -

Асинхронный код, установленный этими функциями, выполняется в основном потоке (по истечении указанного им таймера).

- -
-

Важно знать, что вы можете (и часто будете) запускать другой код до выполнения вызова setTimeout () или между итерациями setInterval (). В зависимости от того, насколько интенсивно используются эти операции для процессора, они могут ещё больше задержать выполнение асинхронного кода, поскольку любой асинхронный код будет выполняться только после того, как станет доступен основной поток. (Другими словами, когда стек пуст.) вы узнаете больше по этому вопросу по мере изучения этой статьи.

-
- -

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

- -

setTimeout()

- -

Как мы ранее отметили, setTimeout () выполняет определённый блок кода один раз по истечении заданного времени. Принимает следующие параметры:

- - - -
-

NOTE:  Указанное время (или задержка) не является гарантированным временем выполнения, а скорее минимальным временем выполнения. Обратные вызовы, которые вы передаёте этим функциям, не могут выполняться, пока стек в основном потоке не станет пустым.

- -

Как следствие, такой код, как setTimeout (fn, 0), будет выполняться, как только стек будет пуст, а не сразу. Если вы выполните такой код, как setTimeout (fn, 0), но сразу после выполнения цикла, который насчитывает от 1 до 10 миллиардов, ваш колбэк будет выполнен через несколько секунд.

-
- -

В следующем примере, браузер будет ожидать две секунды перед тем как  выполнит анонимную функцию, тогда отобразит сообщение (живой пример, и исходный код):

- -
let myGreeting = setTimeout(function() {
-  alert('Hello, Mr. Universe!');
-}, 2000)
- -

Указанные вами функции не обязательно должны быть анонимными. Вы можете дать своей функции имя и даже определить её где-нибудь ещё и передать ссылку на функцию в setTimeout (). Следующие две версии фрагмента кода эквивалентны первой:

- -
// С именованной функцией
-let myGreeting = setTimeout(function sayHi() {
-  alert('Hello, Mr. Universe!');
-}, 2000)
-
-// С функцией определённой отдельно
-function sayHi() {
-  alert('Hello Mr. Universe!');
-}
-
-let myGreeting = setTimeout(sayHi, 2000);
- -

Это может быть полезно, если у вас есть функция, которую нужно вызывать как по таймауту, так например и в ответ на событие. Но это также может  помочь поддерживать ваш код в чистоте, особенно если колбэк тайм-аута занимает больше, чем несколько строк кода.

- -

setTimeout () возвращает значение идентификатора, которое можно использовать для ссылки на тайм-аут позже, например, когда вы хотите его остановить.

- -

Передача параметров в функцию setTimeout ()

- -

Любые параметры, которые вы хотите передать функции, выполняемой внутри setTimeout (), должны быть переданы ей как дополнительные параметры в конце списка.

- -

Например, вы можете реорганизовать предыдущую функцию, чтобы она передавала привет любому имени, переданному ей:

- -
function sayHi(who) {
-  alert(`Hello ${who}!`);
-}
- -

Теперь вы можете передать имя в вызов setTimeout () в качестве третьего параметра:

- -
let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');
- -

Очистка таймаутов

- -

Наконец, если был создан тайм-аут, вы можете отменить его до истечения указанного времени, вызвав clearTimeout(), передав ему идентификатор вызова setTimeout() в качестве параметра. Итак, чтобы отменить указанный выше тайм-аут, вы должны сделать следующее:

- -
clearTimeout(myGreeting);
- -
-

Note: См.greeter-app.html для более полной демонстрации, которая позволяет вам указать имя для приветствия и отменить приветствие с помощью отдельной кнопки (см. исходный код).

-
- -

setInterval()

- -

setTimeout () отлично работает, когда вам нужно один раз запустить код по истечении заданного периода времени. Но что происходит, когда вам нужно запускать код снова и снова - например, в случае анимации?

- -

Здесь пригодится setInterval() . Работает очень похоже на setTimeout (), за исключением того, что функция, которую вы передаёте в качестве первого параметра, выполняется повторно не менее чем за количество миллисекунд, заданных вторым параметром. Вы также можете передать любые параметры, необходимые для выполняемой функции, в качестве последующих параметров вызова setInterval ().

- -

Давайте посмотрим на пример. Следующая функция создаёт новый объект Date(), с помощью toLocaleTimeString() извлекает из него строку с временем и отображает её в пользовательском интерфейсе. Затем он запускает функцию один раз в секунду с помощью setInterval(), создавая эффект цифровых часов, которые обновляются раз в секунду ( реальный пример, и исходный код):

- -
function displayTime() {
-   let date = new Date();
-   let time = date.toLocaleTimeString();
-   document.getElementById('demo').textContent = time;
-}
-
-const createClock = setInterval(displayTime, 1000);
- -

Как и setTimeout (), setInterval () возвращает определённое значение, которое вы можете использовать позже, когда вам нужно очистить интервал.

- -

Очистка интервала

- -

setInterval () выполняет задачу постоянно. setInterval () продолжает выполнять задачу вечно, если вы что-то с ней не сделаете. Возможно, вам понадобится способ остановить такие задачи, иначе вы можете получить ошибки, если браузер не сможет выполнить какие-либо другие версии задачи или если анимация, обрабатываемая задачей, завершилась. Вы можете сделать это так же, как останавливаете timeouts - передавая идентификатор, возвращаемый вызовом setInterval (), в функцию clearInterval ():

- -
const myInterval = setInterval(myFunction, 2000);
-
-clearInterval(myInterval);
- -

Активное обучение: Создание собственного секундомера!

- -

Учитывая все вышесказанное, у нас есть для вас задача. Возьмите копию нашего примера setInterval-clock.html , и измените её так, чтобы создать свой собственный простой секундомер.

- -

Вам нужно отображать время, как и раньше, но в этом примере вам нужно:

- - - -

Несколько подсказок для вас:

- - - -
-

Note: Если вы застряли, вы можете увидеть нашу версию (см. также исходный код ).

-
- -

Что нужно помнить о setTimeout () и setInterval ()

- -

При работе с setTimeout () и setInterval () следует помнить о нескольких вещах. Давайте рассмотрим их.

- -

Рекурсивные таймауты

- -

Есть ещё один способ использования setTimeout (): вы можете вызвать его рекурсивно для повторного запуска одного и того же кода вместо использования setInterval ().

- -

В приведённом ниже примере используется рекурсивный setTimeout () для запуска переданной функции каждые 100 миллисекунд:

- -
let i = 1;
-
-setTimeout(function run() {
-  console.log(i);
-  i++;
-  setTimeout(run, 100);
-}, 100);
- -

Сравните приведённый выше пример со следующим - здесь используется setInterval () для достижения того же эффекта:

- -
let i = 1;
-
-setInterval(function run() {
-  console.log(i);
-  i++
-}, 100);
- -

Чем рекурсивный setTimeout () отличается от setInterval () ?

- -

Разница между двумя версиями приведённого выше кода невелика.

- - - -

Когда ваш код потенциально может занять больше времени, чем назначенный вами интервал времени, лучше использовать рекурсивный setTimeout () - это сохранит постоянный временной интервал между выполнениями независимо от того, сколько времени потребуется для выполнения кода, и вы избежите ошибок.

- -

Немедленные таймауты

- -

Использование 0 в качестве значения для setTimeout () позволяет планировать выполнение указанной колбэк-функции как можно скорее, но только после того, как будет запущен основной поток кода.

- -

Например, код приведённый ниже (рабочий код) выводит alert содержащий "Hello", затем alert содержащий "World" как только вы нажмёте ОК в первом alert.

- -
setTimeout(function() {
-  alert('World');
-}, 0);
-
-alert('Hello');
- -

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

- -

Очистка с помощью clearTimeout() или clearInterval()

- -

clearTimeout () и clearInterval () используют один и тот же список записей для очистки. Интересно, что это означает, что вы можете использовать любой метод для очистки setTimeout () или setInterval ().

- -

Для согласованности следует использовать clearTimeout () для очистки записей setTimeout () и clearInterval () для очистки записей setInterval (). Это поможет избежать путаницы.

- -

requestAnimationFrame()

- -

requestAnimationFrame() это специализированная функция цикла, созданная для эффективного запуска анимации в браузере. По сути, это современная версия setInterval () - она выполняет указанный блок кода до того, как браузер перерисовывает изображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.

- -

Он был создан в ответ на проблемы с setInterval (), который, например, не работает с частотой кадров, оптимизированной для устройства, иногда пропускает кадры, продолжает работать, даже если вкладка не является активной вкладкой или анимация прокручивается со страницы и т. д.(Читай об этом больше в CreativeJS.)

- -
-

Note: вы можете найти примеры использования requestAnimationFrame() в этом курсе — например в  Рисование графики, and Практика построения объектов.

-
- -

Метод принимает в качестве аргумента колбэк, который должен быть вызван перед перерисовкой. Это общий шаблон, в котором он используется:

- -
function draw() {
-   // Drawing code goes here
-   requestAnimationFrame(draw);
-}
-
-draw();
- -

Идея состоит в том, чтобы определить функцию, в которой ваша анимация обновляется (например, ваши спрайты перемещаются, счёт обновляется, данные обновляются или что-то ещё). Затем вы вызываете его, чтобы начать процесс. В конце функционального блока вы вызываете requestAnimationFrame () со ссылкой на функцию, переданной в качестве параметра, и это даёт браузеру указание вызвать функцию снова при следующей перерисовке дисплея. Затем он выполняется непрерывно, поскольку код рекурсивно вызывает requestAnimationFrame ().

- -
-

Note: Если вы хотите выполнить простое постоянное анимирование DOM , CSS Анимация вероятно будет быстрее. Она вычисляется непосредственно внутренним кодом браузера, а не JavaScript.

- -

Однако, если вы делаете что-то более сложное, включающее объекты, которые не доступны напрямую в the DOM (такие как 2D Canvas API или WebGL ), requestAnimationFrame() предпочтительный вариант в большинстве случаев.

-
- -

Как быстро работает ваша анимация?

- -

Плавность анимации напрямую зависит от частоты кадров анимации и измеряется в кадрах в секунду (fps). Чем выше это число, тем плавное будет выглядеть ваша анимация до точки.

- -

Поскольку большинство экранов имеют частоту обновления 60 Гц, максимальная частота кадров, к которой вы можете стремиться, составляет 60 кадров в секунду (FPS) при работе с веб-браузерами. Однако большее количество кадров означает больше обработки, которая часто может вызывать заикание и пропуски, также известные как пропадание кадров или заедание.

- -

Если у вас есть монитор с частотой обновления 60 Гц и вы хотите достичь 60 кадров в секунду, у вас есть около 16,7 миллисекунд (1000/60) для выполнения кода анимации для рендеринга каждого кадра. Это напоминание о том, что вам нужно помнить об объёме кода, который вы пытаетесь запустить во время каждого прохождения цикла анимации.

- -

requestAnimationFrame () всегда пытается приблизиться к этому волшебному значению 60 FPS, насколько это возможно. Иногда это невозможно - если у вас действительно сложная анимация и вы запускаете её на медленном компьютере, частота кадров будет меньше. Во всех случаях requestAnimationFrame () всегда будет делать все возможное с тем, что у него есть.

- -

Чем отличается requestAnimationFrame() от setInterval() and setTimeout()?

- -

Давайте поговорим ещё немного о том, чем метод requestAnimationFrame () отличается от других методов, используемых ранее. Глядя на наш код сверху:

- -
function draw() {
-   // Drawing code goes here
-   requestAnimationFrame(draw);
-}
-
-draw();
- -

Такой же код с использованием setInterval():

- -
function draw() {
-   // Drawing code goes here
-}
-
-setInterval(draw, 17);
- -

Как мы уже говорили ранее, вы не указываете временной интервал для requestAnimationFrame (). Просто он работает максимально быстро и плавно в текущих условиях. Браузер также не тратит время на запуск, если по какой-то причине анимация выходит за пределы экрана и т. д.

- -

setInterval (), с другой стороны, требует указания интервала. Мы пришли к нашему окончательному значению 17 по формуле 1000 миллисекунд / 60 Гц, а затем округлили его в большую сторону. Округление - хорошая идея; если вы округлите в меньшую сторону, браузер может попытаться запустить анимацию со скоростью, превышающей 60 кадров в секунду, и в любом случае это не повлияет на плавность анимации. Как мы уже говорили, стандартная частота обновления - 60 Гц.

- -

В том числе временная метка

- -

Фактическому колбэку, переданному в функцию requestAnimationFrame (), также может быть задан параметр: значение отметки времени, которое представляет время с момента начала работы requestAnimationFrame ().

- -

Это полезно, поскольку позволяет запускать вещи в определённое время и в постоянном темпе, независимо от того, насколько быстрым или медленным может быть ваше устройство. Общий шаблон, который вы бы использовали, выглядит примерно так:

- -
let startTime = null;
-
-function draw(timestamp) {
-    if (!startTime) {
-      startTime = timestamp;
-    }
-
-   currentTime = timestamp - startTime;
-
-   // Do something based on current time
-
-   requestAnimationFrame(draw);
-}
-
-draw();
- -

Поддержка браузерами

- -

requestAnimationFrame () поддерживается в более поздних версиях браузеров, чем setInterval () / setTimeout (). Интересно, что он доступен в Internet Explorer 10 и выше.

- -

Итак, если вам не требуется поддержка старых версий IE, нет особых причин не использовать requestAnimationFrame().

- -

Простой пример

- -

Хватит теории! Давайте выполним упражнение с использованием requestAnimationFrame() . Создадим простую анимацию "spinner animation"—вы могли её видеть в приложениях когда происходят задержки при ответе с сервера и т.п..

- -
-

Note: Для такой простой анимации, вам следовало бы использовать CSS . Однако такой вид анимации очень полезен для демонстрации requestAnimationFrame() , вы скорее всего будете использовать этот метод когда делаете что-то более сложное, например обновление отображения игры в каждом кадре.

-
- -
    -
  1. -

    Возьмите базовый HTML шаблон (такой как этот).

    -
  2. -
  3. -

    Поместите пустой  {{htmlelement("div")}} элемент внутри элемента {{htmlelement("body")}}, затем добавьте внутрь символ ↻ . Этот символ будет действовать как spinner в нашем примере.

    -
  4. -
  5. -

    Примените следующий CSS к HTML шаблону (любым предпочитаемым способом). Он установ красный фон на странице, высоту <body> равную 100% высоты {{htmlelement("html")}} , и центрирует <div> внутри <body>, по горизонтали и вертикали.

    - -
    html {
    -  background-color: white;
    -  height: 100%;
    -}
    -
    -body {
    -  height: inherit;
    -  background-color: red;
    -  margin: 0;
    -  display: flex;
    -  justify-content: center;
    -  align-items: center;
    -}
    -
    -div {
    -  display: inline-block;
    -  font-size: 10rem;
    -}
    -
  6. -
  7. -

    Разместите  {{htmlelement("script")}} элемент перед </body> .

    -
  8. -
  9. -

    Разместите следующий JavaScript-код в  <script> . Здесь вы сохраняете ссылку на <div> внутри, устанавливаете для переменной rotateCount значение 0, устанавливаете неинициализированную переменную, которая позже будет использоваться для хранения ссылки на вызов requestAnimationFrame(), и устанавливаете для переменной startTime значение null, которая будет позже использоваться для хранения времени начала requestAnimationFrame().

    - -
    const spinner = document.querySelector('div');
    -let rotateCount = 0;
    -let startTime = null;
    -let rAF;
    -
    -
  10. -
  11. -

    Под предыдущим кодом вставьте функцию draw() которая будет использоваться для хранения нашего кода анимации, который включает параметр timestamp :

    - -
    function draw(timestamp) {
    -
    -}
    -
  12. -
  13. -

    Внутри draw () добавьте следующие строки. Они определят время начала, если оно ещё не определено (это произойдёт только на первой итерации цикла), и установят для параметра rotateCount значение для поворота счётчика (текущая временная метка, возьмите начальную временную метку, разделённую на три, чтобы замедлиться):

    - -
      if (!startTime) {
    -   startTime = timestamp;
    -  }
    -
    -  rotateCount = (timestamp - startTime) / 3;
    -
    -
  14. -
  15. -

    Под предыдущей строкой внутри draw () добавьте следующий блок - он проверяет, превышает ли значение rotateCount 359 (например, 360, полный круг). Если это так, он устанавливает значение по модулю 360 (то есть остаток, оставшийся после деления значения на 360), поэтому круговая анимация может продолжаться непрерывно с разумным низким значением. Обратите внимание, что это не является строго необходимым, но легче работать со значениями от 0 до 359 градусов, чем со значениями типа «128000 градусов».

    - -
    if (rotateCount > 359) {
    -  rotateCount %= 360;
    -}
    -
  16. -
  17. Затем, под предыдущим блоком, добавьте следующую строку, чтобы вращать spinner: -
    spinner.style.transform = `rotate(${rotateCount}deg)`;
    -
  18. -
  19. -

    В самом низу внутри функции draw () вставьте следующую строку. Это ключ ко всей операции - вы устанавливаете для переменной, определённой ранее, активный вызов requestAnimation (), который принимает функцию draw () в качестве своего параметра. Это запускает анимацию, постоянно выполняя функцию draw () со скоростью, близкой к 60 FPS.

    - -
    rAF = requestAnimationFrame(draw);
    -
  20. -
  21. -

    Ниже, вызовите функцию draw() для запуска анимации.

    - -
    draw();
    -
  22. -
- -
-

Note: вы можете посмотреть рабочий образец на GitHub. ( исходный код.)

-
- -

Очистка вызова  requestAnimationFrame() 

- -

Очистить вызов requestAnimationFrame () можно, вызвав соответствующий метод cancelAnimationFrame (). (Обратите внимание, что имя функции начинается с «cancel», а не «clear», как у методов «set ...».)

- -

Просто передайте ему значение, возвращаемое вызовом requestAnimationFrame () для отмены, которое вы сохранили в переменной rAF:

- -
cancelAnimationFrame(rAF);
- -

Активное обучение: запуск и остановка нашей анимации

- -

В этом упражнении мы хотели бы, чтобы вы протестировали метод cancelAnimationFrame (), взяв наш предыдущий пример и обновив его, добавив обработчик событий для запуска и остановки счётчика при щелчке мышью в любом месте страницы.

- -

Подсказки:

- - - -
-

Note: Для начала попробуйте сами; если вы действительно застряли, посмотрите наш живой пример и исходный код.

-
- -

Регулировка анимации requestAnimationFrame() 

- -

Одним из ограничений requestAnimationFrame () является то, что вы не можете выбирать частоту кадров. В большинстве случаев это не проблема, так как обычно вы хотите, чтобы ваша анимация работала как можно плавное. Но как насчёт того, чтобы создать олдскульную 8-битную анимацию?

- -

Это было проблемой, например в анимации ходьбы, вдохновлённой островом обезьян, из статьи Drawing Graphics:

- -

{{EmbedGHLiveSample("learning-area/javascript/apis/drawing-graphics/loops_animation/7_canvas_walking_animation.html", '100%', 260)}}

- -

В этом примере вы должны анимировать как положение персонажа на экране, так и отображаемый спрайт. В анимации спрайта всего 6 кадров. Если бы вы показывали разные кадры спрайта для каждого кадра, отображаемого на экране, с помощью requestAnimationFrame (), Guybrush двигал бы конечностями слишком быстро, и анимация выглядела бы нелепо. Следовательно, в этом примере регулируется скорость, с которой спрайт циклически повторяет свои кадры, используя следующий код:

- -
if (posX % 13 === 0) {
-  if (sprite === 5) {
-    sprite = 0;
-  } else {
-    sprite++;
-  }
-}
- -

Таким образом, код циклически повторяет спрайт только один раз каждые 13 кадров анимации.

- -

... Фактически, это примерно каждые 6,5 кадров, поскольку мы обновляем posX (положение персонажа на экране) на два кадра:

- -
if (posX > width/2) {
-  newStartPos = -( (width/2) + 102 );
-  posX = Math.ceil(newStartPos / 13) * 13;
-  console.log(posX);
-} else {
-  posX += 2;
-}
- -

Это код, который вычисляет, как обновлять позицию в каждом кадре анимации.

- -

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

- -

Активное обучение: игра на реакцию

- -

В последнем разделе этой статьи вы создадите игру на реакцию для двух игроков. В игре будет два игрока, один из которых управляет игрой с помощью клавиши A, а другой - с помощью клавиши L.

- -

При нажатии кнопки «Start» счётчик, подобный тому, что мы видели ранее, отображается в течение случайного промежутка времени от 5 до 10 секунд. По истечении этого времени появится сообщение «PLAYERS GO !!» - как только это произойдёт, первый игрок, который нажмёт свою кнопку управления, выиграет игру.

- -

{{EmbedGHLiveSample("learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html", '100%', 500)}}

- -

Давайте поработаем над этим:

- -
    -
  1. -

    Прежде всего, скачайте стартовый файл. Он содержит законченную структуру HTML и стили CSS, что даёт нам игровую доску, которая показывает информацию двух игроков (как показано выше), но с счётчиком и параграфом результатов, отображаемыми друг над другом. Вам нужно просто написать JavaScript-код.

    -
  2. -
  3. -

    Внутри пустого элемента {{htmlelement("script")}} на вашей странице, начните с добавления следующих строк кода, которые определяют некоторые переменные и константы, которые вам понадобятся в дальнейшем:

    - -
    const spinner = document.querySelector('.spinner p');
    -const spinnerContainer = document.querySelector('.spinner');
    -let rotateCount = 0;
    -let startTime = null;
    -let rAF;
    -const btn = document.querySelector('button');
    -const result = document.querySelector('.result');
    - -

    В следующем порядке:

    - -
      -
    1. Ссылка на спиннер, чтобы вы могли его анимировать.
    2. -
    3. Ссылка на элемент {{htmlelement("div")}} содержащий спиннер, используемый для отображения и скрытия.
    4. -
    5. Счётчик поворотов. Он определяет, на сколько вы хотите показывать вращение спиннера на каждом кадре анимации.
    6. -
    7. Нулевое время начала. Это будет заполнено временем начала, когда счётчик начнёт вращаться.
    8. -
    9. Неинициализированная переменная для последующего хранения вызова {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} который анимирует спиннер.
    10. -
    11. Ссылка на кнопку Start .
    12. -
    13. Ссылка на параграф результатов.
    14. -
    -
  4. -
  5. -

    Ниже добавьте следующую функцию. Она просто берёт два числа и возвращает случайное число между ними. Это понадобится вам позже, чтобы сгенерировать случайный интервал ожидания.

    - -
    function random(min,max) {
    -  var num = Math.floor(Math.random()*(max-min)) + min;
    -  return num;
    -}
    -
  6. -
  7. -

    Затем добавьте функцию draw(), которая анимирует спиннер. Это очень похоже на версию из предыдущего примера простого счётчика:

    - -
    function draw(timestamp) {
    -  if(!startTime) {
    -   startTime = timestamp;
    -  }
    -
    -  rotateCount = (timestamp - startTime) / 3;
    -
    -  if(rotateCount > 359) {
    -    rotateCount %= 360;
    -  }
    -
    -  spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
    -  rAF = requestAnimationFrame(draw);
    -}
    -
  8. -
  9. -

    Теперь пришло время настроить начальное состояние приложения при первой загрузке страницы. Добавьте следующие две строки, которые просто скрывают абзац результатов и контейнер счётчика с помощью display: none ;.

    - -
    result.style.display = 'none';
    -spinnerContainer.style.display = 'none';
    -
  10. -
  11. -

    Затем определите функцию reset (), которая возвращает приложение в исходное состояние, необходимое для повторного запуска игры после её завершения. Добавьте в конец кода следующее:

    - -
    function reset() {
    -  btn.style.display = 'block';
    -  result.textContent = '';
    -  result.style.display = 'none';
    -}
    -
  12. -
  13. -

    Хорошо, хватит подготовки! Пришло время сделать игру доступной! Добавьте в свой код следующий блок. Функция start () вызывает draw (), чтобы запустить вращение спиннера и отобразить его в пользовательском интерфейсе, скрыть кнопку Start, чтобы вы не могли испортить игру, запустив её несколько раз одновременно, и запускает вызов setTimeout (), который выполняется функция setEndgame () по прошествии случайного интервала от 5 до 10 секунд. Следующий блок также добавляет обработчик событий к вашей кнопке для запуска функции start () при её нажатии.

    - -
    btn.addEventListener('click', start);
    -
    -function start() {
    -  draw();
    -  spinnerContainer.style.display = 'block';
    -  btn.style.display = 'none';
    -  setTimeout(setEndgame, random(5000,10000));
    -}
    - -
    -

    Note: вы увидите, что этот пример вызывает setTimeout() без сохранения возвращаемого значения. (не  let myTimeout = setTimeout(functionName, interval).) 

    - -

    Это прекрасно работает, если вам не нужно очищать интервал / тайм-аут в любой момент. Если вы это сделаете, вам нужно будет сохранить возвращённый идентификатор!

    -
    - -

    Конечным результатом предыдущего кода является то, что при нажатии кнопки «Start» отображается спиннер, и игроки вынуждены ждать произвольное количество времени, прежде чем их попросят нажать их кнопку. Эта последняя часть обрабатывается функцией setEndgame (), которую вы определите позже.

    -
  14. -
  15. -

    Добавьте в свой код следующую функцию:

    - -
    function setEndgame() {
    -  cancelAnimationFrame(rAF);
    -  spinnerContainer.style.display = 'none';
    -  result.style.display = 'block';
    -  result.textContent = 'PLAYERS GO!!';
    -
    -  document.addEventListener('keydown', keyHandler);
    -
    -  function keyHandler(e) {
    -    let isOver = false;
    -    console.log(e.key);
    -
    -    if (e.key === "a") {
    -      result.textContent = 'Player 1 won!!';
    -      isOver = true;
    -    } else if (e.key === "l") {
    -      result.textContent = 'Player 2 won!!';
    -      isOver = true;
    -    }
    -
    -    if (isOver) {
    -      document.removeEventListener('keydown', keyHandler);
    -      setTimeout(reset, 5000);
    -    }
    -  };
    -}
    - -

    Выполните следующие инструкции:

    - -
      -
    1. Во-первых, отмените анимацию спиннера с помощью {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}} (всегда полезно очистить ненужные процессы), и скройте контейнер счётчика.
    2. -
    3. Затем, отобразите абзац с результатами и установите для его текстового содержимого значение "PLAYERS GO!!"  чтобы сообщить игрокам, что теперь они могут нажать свою кнопку, чтобы победить.
    4. -
    5. Прикрепите к документу обработчик событий keydown . При нажатии любой кнопки запускается функция keyHandler().
    6. -
    7. Внутри keyHandler(), код включает объект события в качестве параметра (представленного e) — его свойство {{domxref("KeyboardEvent.key", "key")}} содержит только что нажатую клавишу, и вы можете использовать это для ответа на определённые нажатия клавиш определёнными действиями.
    8. -
    9. Установите для переменной isOver значение false, чтобы мы могли отслеживать, были ли нажаты правильные клавиши, чтобы игрок 1 или 2 выиграл. Мы не хотим, чтобы игра заканчивалась при нажатии неправильной клавиши.
    10. -
    11. Регистрация e.key в консоли, это полезный способ узнать значение различных клавиш, которые вы нажимаете.
    12. -
    13. Когда e.key принимает значение "a", отобразить сообщение о том, что Player 1 выиграл, а когда e.key это "l", отобразить сообщение о том, что Player 2 выиграл. (Note: Это будет работать только со строчными буквами a и l — если переданы прописные A или L , это считается другими клавишами!) Если была нажата одна из этих клавиш, установите для isOver значение true.
    14. -
    15. Только если isOver равно true, удалите обработчик событий keydown с помощью {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} чтобы после того, как произошло выигрышное нажатие, больше не было возможности ввода с клавиатуры, чтобы испортить финальный результат игры. Вы также используете setTimeout() для вызова reset() через 5 секунд — как объяснялось ранее, эта функция сбрасывает игру обратно в исходное состояние, чтобы можно было начать новую игру.
    16. -
    -
  16. -
- -

Вот и все - вы справились!

- -
-

Note: Если вы где то застряли, взгляните на наша версия игры (см. также исходный код ).

-
- -

Заключение

- -

Вот и все — все основы асинхронных циклов и интервалов рассмотрены в статье. Вы найдёте эти методы полезными во многих ситуациях, но постарайтесь не злоупотреблять ими! Поскольку они по-прежнему выполняются в основном потоке, тяжёлые и интенсивные колбэки (особенно те, которые управляют DOM) могут действительно замедлить страницу, если вы не будете осторожны.

- -

{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}

- -

В этом модуле

- - - -
-
-
-- cgit v1.2.3-54-g00ecf