1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
|
---
title: 'Объединённый асинхронный JavaScript: Таймауты и интервалы'
slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals
translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals
original_slug: Learn/JavaScript/Asynchronous/Таймауты_и_интервалы
---
<div>{{LearnSidebar}}</div>
<div>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}</div>
<p class="summary">В этом руководстве рассматриваются традиционные методы, доступные в JavaScript для асинхронного выполнения кода по истечении заданного периода времени или через регулярный интервал (например, заданное количество раз в секунду), обсуждаются их полезные свойства и рассматриваются присущие им проблемы. .</p>
<table class="learn-box standard-table">
<tbody>
<tr>
<th scope="row">Необходимые условия:</th>
<td>Базовая компьютерная грамотность, достаточное понимание основ JavaScript.</td>
</tr>
<tr>
<th scope="row">Цель:</th>
<td>Понимание асинхронных циклов и интервалов, и то как их можно использовать.</td>
</tr>
</tbody>
</table>
<h2 id="Введение">Введение</h2>
<p>В течение долгого времени веб-платформа предлагала программистам JavaScript ряд функций, которые позволяли им асинхронно выполнять код по истечении определённого временного интервала и повторно выполнять асинхронный блок кода, пока вы не скажете ему остановиться.</p>
<p>Эти функции:</p>
<dl>
<dt><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code></dt>
<dd>Выполняет указанный блок кода один раз по истечении указанного времени</dd>
<dt><code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval()</a></code></dt>
<dd>Выполняет указанный блок кода несколько раз с определённым интервалом между каждым вызовом.</dd>
<dt><code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code></dt>
<dd>Современная версия setInterval (). Выполняют указанный блок кода перед тем, как браузер в следующий раз перерисовывает отображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.</dd>
</dl>
<p>Асинхронный код, установленный этими функциями, выполняется в основном потоке (по истечении указанного им таймера).</p>
<div>
<p>Важно знать, что вы можете (и часто будете) запускать другой код до выполнения вызова setTimeout () или между итерациями setInterval (). В зависимости от того, насколько интенсивно используются эти операции для процессора, они могут ещё больше задержать выполнение асинхронного кода, поскольку любой асинхронный код будет выполняться только после того, как станет доступен основной поток. (Другими словами, когда стек пуст.) Вы узнаете больше по этому вопросу по мере изучения этой статьи.</p>
</div>
<p>В любом случае эти функции используются для запуска постоянной анимации и другой фоновой обработки на веб-сайте или в приложении. В следующих разделах мы покажем вам, как их можно использовать.</p>
<h2 id="setTimeout">setTimeout()</h2>
<p>Как мы ранее отметили, setTimeout () выполняет определённый блок кода один раз по истечении заданного времени. Принимает следующие параметры:</p>
<ul>
<li>Функция для запуска или ссылка на функцию, определённую в другом месте.</li>
<li>Число, представляющее интервал времени в миллисекундах (1000 миллисекунд равняется 1 секунде) ожидания перед выполнением кода. Если вы укажете значение 0 (или просто опустите значение), функция запустится как можно скорее. (См. Примечание ниже о том, почему он запускается «как можно скорее», а не «сразу».) Подробнее о том, почему вы, возможно, захотите сделать это позже.</li>
<li>Значений, представляющие любые параметры, которые вы хотите передать функции при её запуске.</li>
</ul>
<div class="blockIndicator note">
<p><strong>NOTE: </strong> Указанное время (или задержка) не является гарантированным временем выполнения, а скорее минимальным временем выполнения. Обратные вызовы, которые вы передаёте этим функциям, не могут выполняться, пока стек в основном потоке не станет пустым.</p>
<p>Как следствие, такой код, как setTimeout (fn, 0), будет выполняться, как только стек будет пуст, а не сразу. Если вы выполните такой код, как setTimeout (fn, 0), но сразу после выполнения цикла, который насчитывает от 1 до 10 миллиардов, ваш колбэк будет выполнен через несколько секунд.</p>
</div>
<p>В следующем примере, браузер будет ожидать две секунды перед тем как выполнит анонимную функцию, тогда отобразит сообщение (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">живой пример</a>, и <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">исходный код</a>):</p>
<pre class="brush: js notranslate">let myGreeting = setTimeout(function() {
alert('Hello, Mr. Universe!');
}, 2000)</pre>
<p>Указанные вами функции не обязательно должны быть анонимными. Вы можете дать своей функции имя и даже определить её где-нибудь ещё и передать ссылку на функцию в setTimeout (). Следующие две версии фрагмента кода эквивалентны первой:</p>
<pre class="brush: js notranslate">// С именованной функцией
let myGreeting = setTimeout(function sayHi() {
alert('Hello, Mr. Universe!');
}, 2000)
// С функцией определённой отдельно
function sayHi() {
alert('Hello Mr. Universe!');
}
let myGreeting = setTimeout(sayHi, 2000);</pre>
<p>Это может быть полезно, если у вас есть функция, которую нужно вызывать как по таймауту, так например и в ответ на событие. Но это также может помочь поддерживать ваш код в чистоте, особенно если колбэк тайм-аута занимает больше, чем несколько строк кода.</p>
<p><code>setTimeout () </code>возвращает значение идентификатора, которое можно использовать для ссылки на тайм-аут позже, например, когда вы хотите его остановить.</p>
<h3 id="Передача_параметров_в_функцию_setTimeout">Передача параметров в функцию setTimeout ()</h3>
<p>Любые параметры, которые вы хотите передать функции, выполняемой внутри setTimeout (), должны быть переданы ей как дополнительные параметры в конце списка.</p>
<p>Например, вы можете реорганизовать предыдущую функцию, чтобы она передавала привет любому имени, переданному ей:</p>
<pre class="brush: js notranslate">function sayHi(who) {
alert(`Hello ${who}!`);
}</pre>
<p>Теперь вы можете передать имя в вызов setTimeout () в качестве третьего параметра:</p>
<pre class="brush: js notranslate">let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');</pre>
<h3 id="Очистка_таймаутов">Очистка таймаутов</h3>
<p>Наконец, если был создан тайм-аут, вы можете отменить его до истечения указанного времени, вызвав <code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout">clearTimeout()</a></code>, передав ему идентификатор вызова <code>setTimeout()</code> в качестве параметра. Итак, чтобы отменить указанный выше тайм-аут, вы должны сделать следующее:</p>
<pre class="brush: js notranslate">clearTimeout(myGreeting);</pre>
<div class="blockIndicator note">
<p><strong>Note</strong>: См.<code><a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/greeter-app.html">greeter-app.html</a></code> для более полной демонстрации, которая позволяет вам указать имя для приветствия и отменить приветствие с помощью отдельной кнопки (<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/greeter-app.html">см. исходный код</a>).</p>
</div>
<h2 id="setInterval">setInterval()</h2>
<p><code>setTimeout ()</code> отлично работает, когда вам нужно один раз запустить код по истечении заданного периода времени. Но что происходит, когда вам нужно запускать код снова и снова - например, в случае анимации?</p>
<p>Здесь пригодится <a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">setInterval()</a> . Работает очень похоже на setTimeout (), за исключением того, что функция, которую вы передаёте в качестве первого параметра, выполняется повторно не менее чем за количество миллисекунд, заданных вторым параметром. Вы также можете передать любые параметры, необходимые для выполняемой функции, в качестве последующих параметров вызова setInterval ().</p>
<p>Давайте посмотрим на пример. Следующая функция создаёт новый объект <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date">Date()</a></code>, с помощью <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString">toLocaleTimeString()</a></code> извлекает из него строку с временем и отображает её в пользовательском интерфейсе. Затем он запускает функцию один раз в секунду с помощью <code>setInterval()</code>, создавая эффект цифровых часов, которые обновляются раз в секунду (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-clock.html"> реальный пример</a>, и <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">исходный код</a>):</p>
<pre class="brush: js notranslate">function displayTime() {
let date = new Date();
let time = date.toLocaleTimeString();
document.getElementById('demo').textContent = time;
}
const createClock = setInterval(displayTime, 1000);</pre>
<p>Как и <code>setTimeout ()</code>, <code>setInterval ()</code> возвращает определённое значение, которое вы можете использовать позже, когда вам нужно очистить интервал.</p>
<h3 id="Очистка_интервала">Очистка интервала</h3>
<p><code>setInterval () </code>выполняет задачу постоянно. setInterval () продолжает выполнять задачу вечно, если вы что-то с ней не сделаете. Возможно, вам понадобится способ остановить такие задачи, иначе вы можете получить ошибки, если браузер не сможет выполнить какие-либо другие версии задачи или если анимация, обрабатываемая задачей, завершилась. Вы можете сделать это так же, как останавливаете <code>timeouts</code> - передавая идентификатор, возвращаемый вызовом <code>setInterval ()</code>, в функцию <code>clearInterval ()</code>:</p>
<pre class="brush: js notranslate">const myInterval = setInterval(myFunction, 2000);
clearInterval(myInterval);</pre>
<h4 id="Активное_обучение_Создание_собственного_секундомера!">Активное обучение: Создание собственного секундомера!</h4>
<p>Учитывая все вышесказанное, у нас есть для вас задача. Возьмите копию нашего примера <code><a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">setInterval-clock.html</a></code> , и измените её так, чтобы создать свой собственный простой секундомер.</p>
<p>Вам нужно отображать время, как и раньше, но в этом примере вам нужно:</p>
<ul>
<li>Кнопка "Start" для запуска секундомера.</li>
<li>Кнопка "Stop" для паузы/остановки.</li>
<li>Кнопка "Reset", чтобы сбросить счётчик времени на <code>0</code>.</li>
<li>Дисплей времени, чтобы отображать количество прошедших секунд а не фактическое время.</li>
</ul>
<p>Несколько подсказок для вас:</p>
<ul>
<li>Вы можете структурировать и стилизовать разметку кнопок по своему усмотрению; просто убедитесь, что вы используете семантический HTML с кавычками, которые позволяют захватывать ссылки на кнопки с помощью JavaScript.</li>
<li>Вероятно, вы захотите создать переменную, которая начинается с 0, а затем увеличивается на единицу каждую секунду с использованием постоянного цикла.</li>
<li>Этот пример проще создать без использования объекта Date (), как мы это делали в нашей версии, но он будет менее точен - вы не можете гарантировать, что колбэк сработает ровно через 1000 мс. Более точным способом было бы запустить startTime = Date.now (), чтобы получить метку времени, когда пользователь нажал кнопку запуска, а затем выполнить Date.now () - startTime, чтобы получить количество миллисекунд после того, как была нажата кнопка запуска .</li>
<li>Вам также нужно рассчитать количество часов, минут и секунд как отдельные значения, а затем отображать их вместе в строке после каждой итерации цикла. На втором счётчике вы можете отработать каждую из них.</li>
<li>Как вы могли бы их рассчитать? Подумайте об этом:
<ul>
<li>В одном часе <code>3600 </code>секунд.</li>
<li>Количество минут - это количество секунд, оставшееся после вычитания всех часов, разделённое на 60.</li>
<li>Количество секунд будет количеством секунд, оставшихся после вычитания всех минут.</li>
</ul>
</li>
<li>Вам необходимо включить начальный ноль в отображаемые значения, если сумма меньше <code>10</code>, чтобы они больше походили на традиционные часы.</li>
<li>Чтобы приостановить секундомер, вам нужно очистить интервал. Чтобы сбросить его, вам нудно установить счётчик обратно на <code>0</code>, очистить интервал, а затем немедленно обновить отображение.</li>
<li>Вероятно, вам следует отключить кнопку запуска после её нажатия один раз и снова включить её после того, как вы остановили / сбросили её. В противном случае многократное нажатие кнопки запуска приведёт к применению нескольких <code>setInterval ()</code> к часам, что приведёт к неправильному поведению.</li>
</ul>
<div class="blockIndicator note">
<p><strong>Note</strong>: Если вы застряли, вы можете <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-stopwatch.html">увидеть нашу версию</a> (см. также <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-stopwatch.html">исходный код</a> ).</p>
</div>
<h2 id="Что_нужно_помнить_о_setTimeout_и_setInterval">Что нужно помнить о <code>setTimeout ()</code> и <code>setInterval ()</code></h2>
<p>При работе с<code> setTimeout ()</code> и <code>setInterval ()</code> следует помнить о нескольких вещах. Давайте рассмотрим их.</p>
<h3 id="Рекурсивные_таймауты">Рекурсивные таймауты</h3>
<p>Есть ещё один способ использования <code>setTimeout ()</code>: вы можете вызвать его рекурсивно для повторного запуска одного и того же кода вместо использования <code>setInterval ()</code>.</p>
<p>В приведённом ниже примере используется рекурсивный setTimeout () для запуска переданной функции каждые 100 миллисекунд:</p>
<pre class="brush: js notranslate">let i = 1;
setTimeout(function run() {
console.log(i);
i++;
setTimeout(run, 100);
}, 100);</pre>
<p>Сравните приведённый выше пример со следующим - здесь используется <code>setInterval ()</code> для достижения того же эффекта:</p>
<pre class="brush: js notranslate">let i = 1;
setInterval(function run() {
console.log(i);
i++
}, 100);</pre>
<h4 id="Чем_рекурсивный_setTimeout_отличается_от_setInterval">Чем рекурсивный <code>setTimeout ()</code> отличается от <code>setInterval () </code>?</h4>
<p>Разница между двумя версиями приведённого выше кода невелика.</p>
<ul>
<li>Рекурсивный <code>setTimeout ()</code> гарантирует такую же задержку между выполнениями. (Например, 100 мс в приведённом выше случае.) Код будет запущен, затем подождёт 100 миллисекунд, прежде чем запустится снова, поэтому интервал будет одинаковым, независимо от того, сколько времени требуется для выполнения кода.</li>
<li>Пример с использованием <code>setInterval () </code>работает несколько иначе. Выбранный вами интервал включает время, затрачиваемое на выполнение кода, который вы хотите запустить. Предположим, что выполнение кода занимает <code>40 </code>миллисекунд - тогда интервал составляет всего <code>60 </code>миллисекунд.</li>
<li>При рекурсивном использовании <code>setTimeout ()</code> каждая итерация может вычислять различную задержку перед запуском следующей итерации. Другими словами, значение второго параметра может указывать другое время в миллисекундах для ожидания перед повторным запуском кода.</li>
</ul>
<p>Когда ваш код потенциально может занять больше времени, чем назначенный вами интервал времени, лучше использовать рекурсивный <code>setTimeout ()</code> - это сохранит постоянный временной интервал между выполнениями независимо от того, сколько времени потребуется для выполнения кода, и вы избежите ошибок.</p>
<h3 id="Немедленные_таймауты">Немедленные таймауты</h3>
<p>Использование 0 в качестве значения для <code>setTimeout ()</code> позволяет планировать выполнение указанной колбэк-функции как можно скорее, но только после того, как будет запущен основной поток кода.</p>
<p>Например, код приведённый ниже (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/zero-settimeout.html">рабочий код</a>) выводит alert содержащий <code>"Hello"</code>, затем alert содержащий <code>"World"</code> как только вы нажмёте ОК в первом alert.</p>
<pre class="brush: js notranslate">setTimeout(function() {
alert('World');
}, 0);
alert('Hello');</pre>
<p>Это может быть полезно в тех случаях, когда вы хотите установить блок кода для запуска, как только весь основной поток завершит работу - поместите его в цикл событий async, чтобы он запускался сразу после этого.</p>
<h3 id="Очистка_с_помощью_clearTimeout_или_clearInterval">Очистка с помощью <code>clearTimeout()</code> или <code>clearInterval()</code></h3>
<p>clearTimeout () и <code>clearInterval ()</code> используют один и тот же список записей для очистки. Интересно, что это означает, что вы можете использовать любой метод для очистки setTimeout () или setInterval ().</p>
<p>Для согласованности следует использовать <code>clearTimeout ()</code> для очистки записей <code>setTimeout ()</code> и <code>clearInterval ()</code> для очистки записей <code>setInterval ()</code>. Это поможет избежать путаницы.</p>
<h2 id="requestAnimationFrame">requestAnimationFrame()</h2>
<p><code><a href="/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code> это специализированная функция цикла, созданная для эффективного запуска анимации в браузере. По сути, это современная версия <code>setInterval ()</code> - она выполняет указанный блок кода до того, как браузер перерисовывает изображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.</p>
<p>Он был создан в ответ на проблемы с <code>setInterval ()</code>, который, например, не работает с частотой кадров, оптимизированной для устройства, иногда пропускает кадры, продолжает работать, даже если вкладка не является активной вкладкой или анимация прокручивается со страницы и т. д.(<a href="http://creativejs.com/resources/requestanimationframe/index.html">Читай об этом больше в CreativeJS</a>.)</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: Вы можете найти примеры использования <code>requestAnimationFrame()</code> в этом курсе — например в <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Рисование графики</a>, and <a href="/en-US/docs/Learn/JavaScript/Objects/Object_building_practice">Практика построения объектов</a>.</p>
</div>
<p>Метод принимает в качестве аргумента колбэк, который должен быть вызван перед перерисовкой. Это общий шаблон, в котором он используется:</p>
<pre class="brush: js notranslate">function draw() {
// Drawing code goes here
requestAnimationFrame(draw);
}
draw();</pre>
<p>Идея состоит в том, чтобы определить функцию, в которой ваша анимация обновляется (например, ваши спрайты перемещаются, счёт обновляется, данные обновляются или что-то ещё). Затем вы вызываете его, чтобы начать процесс. В конце функционального блока вы вызываете <code>requestAnimationFrame ()</code> со ссылкой на функцию, переданной в качестве параметра, и это даёт браузеру указание вызвать функцию снова при следующей перерисовке дисплея. Затем он выполняется непрерывно, поскольку код рекурсивно вызывает <code>requestAnimationFrame ().</code></p>
<div class="blockIndicator note">
<p><strong>Note</strong>: Если вы хотите выполнить простое постоянное анимирование DOM , <a href="/en-US/docs/Web/CSS/CSS_Animations">CSS Анимация</a> вероятно будет быстрее. Она вычисляется непосредственно внутренним кодом браузера, а не JavaScript.</p>
<p>Однако, если вы делаете что-то более сложное, включающее объекты, которые не доступны напрямую в the DOM (такие как <a href="/en-US/docs/Web/API/Canvas_API">2D Canvas API</a> или <a href="/en-US/docs/Web/API/WebGL_API">WebGL</a> ), <code>requestAnimationFrame()</code> предпочтительный вариант в большинстве случаев.</p>
</div>
<h3 id="Как_быстро_работает_ваша_анимация">Как быстро работает ваша анимация?</h3>
<p>Плавность анимации напрямую зависит от частоты кадров анимации и измеряется в кадрах в секунду (fps). Чем выше это число, тем плавное будет выглядеть ваша анимация до точки.</p>
<p>Поскольку большинство экранов имеют частоту обновления 60 Гц, максимальная частота кадров, к которой вы можете стремиться, составляет 60 кадров в секунду (FPS) при работе с веб-браузерами. Однако большее количество кадров означает больше обработки, которая часто может вызывать заикание и пропуски, также известные как пропадание кадров или заедание.</p>
<p>Если у вас есть монитор с частотой обновления 60 Гц и вы хотите достичь 60 кадров в секунду, у вас есть около 16,7 миллисекунд <code>(1000/60)</code> для выполнения кода анимации для рендеринга каждого кадра. Это напоминание о том, что вам нужно помнить об объёме кода, который вы пытаетесь запустить во время каждого прохождения цикла анимации.</p>
<p><code>requestAnimationFrame ()</code> всегда пытается приблизиться к этому волшебному значению 60 FPS, насколько это возможно. Иногда это невозможно - если у вас действительно сложная анимация и вы запускаете её на медленном компьютере, частота кадров будет меньше. Во всех случаях<code> requestAnimationFrame ()</code> всегда будет делать все возможное с тем, что у него есть.</p>
<h3 id="Чем_отличается_requestAnimationFrame_от_setInterval_and_setTimeout">Чем отличается requestAnimationFrame() от setInterval() and setTimeout()?</h3>
<p>Давайте поговорим ещё немного о том, чем метод <code>requestAnimationFrame ()</code> отличается от других методов, используемых ранее. Глядя на наш код сверху:</p>
<pre class="brush: js notranslate">function draw() {
// Drawing code goes here
requestAnimationFrame(draw);
}
draw();</pre>
<p>Такой же код с использованием <code>setInterval()</code>:</p>
<pre class="brush: js notranslate">function draw() {
// Drawing code goes here
}
setInterval(draw, 17);</pre>
<p>Как мы уже говорили ранее, вы не указываете временной интервал для <code>requestAnimationFrame ()</code>. Просто он работает максимально быстро и плавно в текущих условиях. Браузер также не тратит время на запуск, если по какой-то причине анимация выходит за пределы экрана и т. д.</p>
<p><code>setInterval ()</code>, с другой стороны, требует указания интервала. Мы пришли к нашему окончательному значению 17 по формуле 1000 миллисекунд / 60 Гц, а затем округлили его в большую сторону. Округление - хорошая идея; если вы округлите в меньшую сторону, браузер может попытаться запустить анимацию со скоростью, превышающей 60 кадров в секунду, и в любом случае это не повлияет на плавность анимации. Как мы уже говорили, стандартная частота обновления - 60 Гц.</p>
<h3 id="В_том_числе_временная_метка">В том числе временная метка</h3>
<p>Фактическому колбэку, переданному в функцию <code>requestAnimationFrame ()</code>, также может быть задан параметр: значение отметки времени, которое представляет время с момента начала работы <code>requestAnimationFrame ().</code></p>
<p>Это полезно, поскольку позволяет запускать вещи в определённое время и в постоянном темпе, независимо от того, насколько быстрым или медленным может быть ваше устройство. Общий шаблон, который вы бы использовали, выглядит примерно так:</p>
<pre class="brush: js notranslate">let startTime = null;
function draw(timestamp) {
if (!startTime) {
startTime = timestamp;
}
currentTime = timestamp - startTime;
// Do something based on current time
requestAnimationFrame(draw);
}
draw();</pre>
<h3 id="Поддержка_браузерами">Поддержка браузерами</h3>
<p><code>requestAnimationFrame ()</code> поддерживается в более поздних версиях браузеров, чем s<code>etInterval ()</code> / <code>setTimeout ()</code>. Интересно, что он доступен в Internet Explorer 10 и выше.</p>
<p>Итак, если вам не требуется поддержка старых версий IE, нет особых причин не использовать <code>requestAnimationFrame()</code>.</p>
<h3 id="Простой_пример">Простой пример</h3>
<p>Хватит теории! Давайте выполним упражнение с использованием <code>requestAnimationFrame()</code> . Создадим простую анимацию "spinner animation"—вы могли её видеть в приложениях когда происходят задержки при ответе с сервера и т.п..</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: Для такой простой анимации, вам следовало бы использовать CSS . Однако такой вид анимации очень полезен для демонстрации <code>requestAnimationFrame()</code> , вы скорее всего будете использовать этот метод когда делаете что-то более сложное, например обновление отображения игры в каждом кадре.</p>
</div>
<ol>
<li>
<p>Возьмите базовый HTML шаблон (<a href="https://github.com/mdn/learning-area/blob/master/html/introduction-to-html/getting-started/index.html">такой как этот</a>).</p>
</li>
<li>
<p>Поместите пустой {{htmlelement("div")}} элемент внутри элемента {{htmlelement("body")}}, затем добавьте внутрь символ ↻ . Этот символ будет действовать как spinner в нашем примере.</p>
</li>
<li>
<p>Примените следующий CSS к HTML шаблону (любым предпочитаемым способом). Он установ красный фон на странице, высоту <code><body></code> равную <code>100%</code> высоты {{htmlelement("html")}} , и центрирует <code><div></code> внутри <code><body></code>, по горизонтали и вертикали.</p>
<pre class="brush: css notranslate">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;
}</pre>
</li>
<li>
<p>Разместите {{htmlelement("script")}} элемент перед <code></body></code> .</p>
</li>
<li>
<p>Разместите следующий JavaScript код в <code><script></code> . Здесь вы сохраняете ссылку на <code><div></code> внутри, устанавливаете для переменной <code>rotateCount</code> значение <code>0</code>, устанавливаете неинициализированную переменную, которая позже будет использоваться для хранения ссылки на вызов <code>requestAnimationFrame()</code>, и устанавливаете для переменной <code>startTime</code> значение <code>null</code>, которая будет позже использоваться для хранения времени начала <code>requestAnimationFrame()</code>.</p>
<pre class="brush: js notranslate">const spinner = document.querySelector('div');
let rotateCount = 0;
let startTime = null;
let rAF;
</pre>
</li>
<li>
<p>Под предыдущим кодом вставьте функцию <code>draw()</code> которая будет использоваться для хранения нашего кода анимации, который включает параметр <code>timestamp</code> :</p>
<pre class="brush: js notranslate">function draw(timestamp) {
}</pre>
</li>
<li>
<p>Внутри <code>draw ()</code> добавьте следующие строки. Они определят время начала, если оно ещё не определено (это произойдёт только на первой итерации цикла), и установят для параметра <code>rotateCount</code> значение для поворота счётчика (текущая временная метка, возьмите начальную временную метку, разделённую на три, чтобы замедлиться):</p>
<pre class="brush: js notranslate"> if (!startTime) {
startTime = timestamp;
}
rotateCount = (timestamp - startTime) / 3;
</pre>
</li>
<li>
<p>Под предыдущей строкой внутри <code>draw ()</code> добавьте следующий блок - он проверяет, превышает ли значение <code>rotateCount 359</code> (например, <code>360</code>, полный круг). Если это так, он устанавливает значение по модулю <code>360</code> (то есть остаток, оставшийся после деления значения на <code>360</code>), поэтому круговая анимация может продолжаться непрерывно с разумным низким значением. Обратите внимание, что это не является строго необходимым, но легче работать со значениями от 0 до <code>359</code> градусов, чем со значениями типа «<code>128000</code> градусов».</p>
<pre class="brush: js notranslate">if (rotateCount > 359) {
rotateCount %= 360;
}</pre>
</li>
<li>Затем, под предыдущим блоком, добавьте следующую строку, чтобы вращать spinner:
<pre class="brush: js notranslate">spinner.style.transform = `rotate(${rotateCount}deg)`;</pre>
</li>
<li>
<p>В самом низу внутри функции <em>draw ()</em> вставьте следующую строку. Это ключ ко всей операции - вы устанавливаете для переменной, определённой ранее, активный вызов<em> requestAnimation ()</em>, который принимает функцию <em>draw ()</em> в качестве своего параметра. Это запускает анимацию, постоянно выполняя функцию <em>draw ()</em> со скоростью, близкой к 60 FPS.</p>
<pre class="brush: js notranslate">rAF = requestAnimationFrame(draw);</pre>
</li>
<li>
<p>Ниже, вызовите функцию <code>draw()</code> для запуска анимации.</p>
<pre class="brush: js notranslate">draw();</pre>
</li>
</ol>
<div class="blockIndicator note">
<p><strong>Note</strong>: Вы можете посмотреть <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">рабочий образец на GitHub</a>. ( <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">исходный код</a>.)</p>
</div>
<h3 id="Очистка_вызова_requestAnimationFrame">Очистка вызова requestAnimationFrame() </h3>
<p>Очистить вызов <code>requestAnimationFrame ()</code> можно, вызвав соответствующий метод <code>cancelAnimationFrame ()</code>. (Обратите внимание, что имя функции начинается с «cancel», а не «clear», как у методов «set ...».)</p>
<p>Просто передайте ему значение, возвращаемое вызовом requestAnimationFrame () для отмены, которое вы сохранили в переменной rAF:</p>
<pre class="brush: js notranslate">cancelAnimationFrame(rAF);</pre>
<h3 id="Активное_обучение_запуск_и_остановка_нашей_анимации">Активное обучение: запуск и остановка нашей анимации</h3>
<p>В этом упражнении мы хотели бы, чтобы вы протестировали метод <code>cancelAnimationFrame ()</code>, взяв наш предыдущий пример и обновив его, добавив обработчик событий для запуска и остановки счётчика при щелчке мышью в любом месте страницы.</p>
<p>Подсказки:</p>
<ul>
<li>Обработчик события щелчка можно добавить к большинству элементов, включая документ <code><body></code>. Имеет смысл поместить его в элемент <code><body></code>, если вы хотите максимизировать интерактивную область - событие всплывает до его дочерних элементов.</li>
<li>Вы захотите добавить переменную отслеживания, чтобы проверить, вращается ли счётчик или нет, очистив кадр анимации, если он есть, и снова вызвать его, если это не так.</li>
</ul>
<div class="blockIndicator note">
<p><strong>Note</strong>: Для начала попробуйте сами; если вы действительно застряли, посмотрите наш <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/start-and-stop-spinner.html">живой пример</a> и <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/start-and-stop-spinner.html">исходный код</a>.</p>
</div>
<h3 id="Регулировка_анимации_requestAnimationFrame">Регулировка анимации <code>requestAnimationFrame()</code> </h3>
<p>Одним из ограничений <code>requestAnimationFrame ()</code> является то, что вы не можете выбирать частоту кадров. В большинстве случаев это не проблема, так как обычно вы хотите, чтобы ваша анимация работала как можно плавное. Но как насчёт того, чтобы создать олдскульную 8-битную анимацию?</p>
<p>Это было проблемой, например в анимации ходьбы, вдохновлённой островом обезьян, из статьи <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Drawing Graphics</a>:</p>
<p>{{EmbedGHLiveSample("learning-area/javascript/apis/drawing-graphics/loops_animation/7_canvas_walking_animation.html", '100%', 260)}}</p>
<p>В этом примере вы должны анимировать как положение персонажа на экране, так и отображаемый спрайт. В анимации спрайта всего 6 кадров. Если бы вы показывали разные кадры спрайта для каждого кадра, отображаемого на экране, с помощью requestAnimationFrame (), Guybrush двигал бы конечностями слишком быстро, и анимация выглядела бы нелепо. Следовательно, в этом примере регулируется скорость, с которой спрайт циклически повторяет свои кадры, используя следующий код:</p>
<pre class="brush: js notranslate">if (posX % 13 === 0) {
if (sprite === 5) {
sprite = 0;
} else {
sprite++;
}
}</pre>
<p>Таким образом, код циклически повторяет спрайт только один раз каждые 13 кадров анимации.</p>
<p>... Фактически, это примерно каждые 6,5 кадров, поскольку мы обновляем posX (положение персонажа на экране) на два кадра:</p>
<pre class="brush: js notranslate">if (posX > width/2) {
newStartPos = -( (width/2) + 102 );
posX = Math.ceil(newStartPos / 13) * 13;
console.log(posX);
} else {
posX += 2;
}</pre>
<p>Это код, который вычисляет, как обновлять позицию в каждом кадре анимации.</p>
<p>Метод, который вы используете для регулирования анимации, будет зависеть от вашего конкретного кода. Например, в предыдущем примере счётчика вы могли заставить его двигаться медленнее, увеличивая rotateCount только на единицу в каждом кадре вместо двух.</p>
<h2 id="Активное_обучение_игра_на_реакцию">Активное обучение: игра на реакцию</h2>
<p>В последнем разделе этой статьи вы создадите игру на реакцию для двух игроков. В игре будет два игрока, один из которых управляет игрой с помощью клавиши <kbd>A</kbd>, а другой - с помощью клавиши<kbd> L</kbd>.</p>
<p>При нажатии кнопки «Start» счётчик, подобный тому, что мы видели ранее, отображается в течение случайного промежутка времени от 5 до 10 секунд. По истечении этого времени появится сообщение «PLAYERS GO !!» - как только это произойдёт, первый игрок, который нажмёт свою кнопку управления, выиграет игру.</p>
<p>{{EmbedGHLiveSample("learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html", '100%', 500)}}</p>
<p>Давайте поработаем над этим:</p>
<ol>
<li>
<p>Прежде всего, скачайте <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/reaction-game-starter.html">стартовый файл</a>. Он содержит законченную структуру HTML и стили CSS, что даёт нам игровую доску, которая показывает информацию двух игроков (как показано выше), но с счётчиком и параграфом результатов, отображаемыми друг над другом. Вам нужно просто написать JavaScript код.</p>
</li>
<li>
<p>Внутри пустого элемента {{htmlelement("script")}} на вашей странице, начните с добавления следующих строк кода, которые определяют некоторые переменные и константы, которые вам понадобятся в дальнейшем:</p>
<pre class="brush: js notranslate">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');</pre>
<p>В следующем порядке:</p>
<ol>
<li>Ссылка на спиннер, чтобы вы могли его анимировать.</li>
<li>Ссылка на элемент {{htmlelement("div")}} содержащий спиннер, используемый для отображения и скрытия.</li>
<li>Счётчик поворотов. Он определяет, на сколько вы хотите показывать вращение спиннера на каждом кадре анимации.</li>
<li>Нулевое время начала. Это будет заполнено временем начала, когда счётчик начнёт вращаться.</li>
<li>Неинициализированная переменная для последующего хранения вызова {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} который анимирует спиннер.</li>
<li>Ссылка на кнопку Start .</li>
<li>Ссылка на параграф результатов.</li>
</ol>
</li>
<li>
<p>Ниже добавьте следующую функцию. Она просто берёт два числа и возвращает случайное число между ними. Это понадобится вам позже, чтобы сгенерировать случайный интервал ожидания.</p>
<pre class="brush: js notranslate">function random(min,max) {
var num = Math.floor(Math.random()*(max-min)) + min;
return num;
}</pre>
</li>
<li>
<p>Затем добавьте функцию draw(), которая анимирует спиннер. Это очень похоже на версию из предыдущего примера простого счётчика:</p>
<pre class="brush: js notranslate">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);
}</pre>
</li>
<li>
<p>Теперь пришло время настроить начальное состояние приложения при первой загрузке страницы. Добавьте следующие две строки, которые просто скрывают абзац результатов и контейнер счётчика с помощью <code>display: none</code> ;.</p>
<pre class="brush: js notranslate">result.style.display = 'none';
spinnerContainer.style.display = 'none';</pre>
</li>
<li>
<p>Затем определите функцию<code> reset ()</code>, которая возвращает приложение в исходное состояние, необходимое для повторного запуска игры после её завершения. Добавьте в конец кода следующее:</p>
<pre class="brush: js notranslate">function reset() {
btn.style.display = 'block';
result.textContent = '';
result.style.display = 'none';
}</pre>
</li>
<li>
<p>Хорошо, хватит подготовки! Пришло время сделать игру доступной! Добавьте в свой код следующий блок. Функция <code>start ()</code> вызывает <code>draw ()</code>, чтобы запустить вращение спиннера и отобразить его в пользовательском интерфейсе, скрыть кнопку Start, чтобы вы не могли испортить игру, запустив её несколько раз одновременно, и запускает вызов <code>setTimeout ()</code>, который выполняется функция <code>setEndgame ()</code> по прошествии случайного интервала от 5 до 10 секунд. Следующий блок также добавляет обработчик событий к вашей кнопке для запуска функции <code>start ()</code> при её нажатии.</p>
<pre class="brush: js notranslate">btn.addEventListener('click', start);
function start() {
draw();
spinnerContainer.style.display = 'block';
btn.style.display = 'none';
setTimeout(setEndgame, random(5000,10000));
}</pre>
<div class="blockIndicator note">
<p><strong>Note</strong>: Вы увидите, что этот пример вызывает <code>setTimeout()</code> без сохранения возвращаемого значения. (не <code>let myTimeout = setTimeout(functionName, interval)</code>.) </p>
<p>Это прекрасно работает, если вам не нужно очищать интервал / тайм-аут в любой момент. Если вы это сделаете, вам нужно будет сохранить возвращённый идентификатор!</p>
</div>
<p>Конечным результатом предыдущего кода является то, что при нажатии кнопки «Start» отображается спиннер, и игроки вынуждены ждать произвольное количество времени, прежде чем их попросят нажать их кнопку. Эта последняя часть обрабатывается функцией <code>setEndgame ()</code>, которую вы определите позже.</p>
</li>
<li>
<p>Добавьте в свой код следующую функцию:</p>
<pre class="brush: js notranslate">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);
}
};
}</pre>
<p>Выполните следующие инструкции:</p>
<ol>
<li>Во-первых, отмените анимацию спиннера с помощью {{domxref("window.cancelAnimationFrame", "cancelAnimationFrame()")}} (всегда полезно очистить ненужные процессы), и скройте контейнер счётчика.</li>
<li>Затем, отобразите абзац с результатами и установите для его текстового содержимого значение "PLAYERS GO!!" чтобы сообщить игрокам, что теперь они могут нажать свою кнопку, чтобы победить.</li>
<li>Прикрепите к документу обработчик событий <code><a href="/en-US/docs/Web/API/Document/keydown_event">keydown</a></code> . При нажатии любой кнопки запускается функция <code>keyHandler()</code>.</li>
<li>Внутри <code>keyHandler()</code>, код включает объект события в качестве параметра (представленного <code>e</code>) — его свойство {{domxref("KeyboardEvent.key", "key")}} содержит только что нажатую клавишу, и вы можете использовать это для ответа на определённые нажатия клавиш определёнными действиями.</li>
<li>Установите для переменной <code>isOver</code> значение false, чтобы мы могли отслеживать, были ли нажаты правильные клавиши, чтобы игрок 1 или 2 выиграл. Мы не хотим, чтобы игра заканчивалась при нажатии неправильной клавиши.</li>
<li>Регистрация <code>e.key</code> в консоли, это полезный способ узнать значение различных клавиш, которые вы нажимаете.</li>
<li>Когда <code>e.key</code> принимает значение "a", отобразить сообщение о том, что Player 1 выиграл, а когда <code>e.key</code> это "l", отобразить сообщение о том, что Player 2 выиграл. (<strong>Note:</strong> Это будет работать только со строчными буквами a и l — если переданы прописные A или L , это считается другими клавишами!) Если была нажата одна из этих клавиш, установите для <code>isOver</code> значение <code>true</code>.</li>
<li>Только если <code>isOver</code> равно <code>true</code>, удалите обработчик событий <code>keydown</code> с помощью {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} чтобы после того, как произошло выигрышное нажатие, больше не было возможности ввода с клавиатуры, чтобы испортить финальный результат игры. Вы также используете <code>setTimeout()</code> для вызова <code>reset()</code> через 5 секунд — как объяснялось ранее, эта функция сбрасывает игру обратно в исходное состояние, чтобы можно было начать новую игру.</li>
</ol>
</li>
</ol>
<p>Вот и все - вы справились!</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: Если вы где то застряли, взгляните на <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html">наша версия игры</a> (см. также <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/reaction-game.html">исходный код</a> ).</p>
</div>
<h2 id="Заключение">Заключение</h2>
<p>Вот и все — все основы асинхронных циклов и интервалов рассмотрены в статье. Вы найдёте эти методы полезными во многих ситуациях, но постарайтесь не злоупотреблять ими! Поскольку они по-прежнему выполняются в основном потоке, тяжёлые и интенсивные колбэки (особенно те, которые управляют DOM) могут действительно замедлить страницу, если вы не будете осторожны.</p>
<p>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}</p>
<h2 id="В_этом_модуле">В этом модуле</h2>
<ul>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Concepts">Основные понятия асинхронного программирования</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">Введение в асинхронный JavaScript</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Объединённый асинхронный JavaScript: Таймауты и интервалы</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises">Graceful asynchronous programming with Promises</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Async_await">Сделайте асинхронное программирование легче с async и await</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">Choosing the right approach</a></li>
</ul>
<div id="gtx-trans" style="position: absolute; left: 70px; top: 13746px;">
<div class="gtx-trans-icon"></div>
</div>
|