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
|
---
title: '合作异步JavaScript: 超时和间隔'
slug: conflicting/Learn/JavaScript/Asynchronous_ae5a561b0ec11fc53c167201aa8af5df
translation_of: Learn/JavaScript/Asynchronous/Timeouts_and_intervals
original_slug: Learn/JavaScript/Asynchronous/Timeouts_and_intervals
---
<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>很长一段时间以来,web平台为JavaScript程序员提供了许多函数,这些函数允许您在一段时间间隔过后异步执行代码,或者重复异步执行代码块,直到您告诉它停止为止。这些都是:</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>
<p>在 <code>setTimeout()</code> 调用执行之前或 <code>setInterval()</code> 迭代之间可以(并且经常会)运行其他代码。根据这些操作的处理器密集程度,它们可以进一步延迟异步代码,因为任何异步代码仅在主线程可用后才执行(换句话说,当调用栈为空时)。在阅读本文时,您将学到更多关于此问题的信息。</p>
<p>无论如何,这些函数用于在web站点或应用程序上运行不间断的动画和其他后台处理。在下面的部分中,我们将向您展示如何使用它们。</p>
<h2 id="setTimeout">setTimeout()</h2>
<p>正如前述, <code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code> 在指定的时间后执行一段特定代码. 它需要如下参数:</p>
<ul>
<li>要运行的函数,或者函数引用。</li>
<li>表示在执行代码之前等待的时间间隔(以毫秒为单位,所以1000等于1秒)的数字。如果指定值为0(或完全省略该值),函数将尽快运行(参阅下面的注释,了解为什么它“尽快”而不是“立即”运行)。稍后详述这样做的原因。</li>
<li>更多的参数:在指定函数运行时,希望传递给函数的值。</li>
</ul>
<div class="blockIndicator note">
<p><strong>Note:</strong> 指定的时间(或延迟)不能保证在指定的确切时间之后执行,而是最短的延迟执行时间。在主线程上的堆栈为空之前,传递给这些函数的回调将无法运行。</p>
<p>结果,像 <code>setTimeout(fn, 0)</code> 这样的代码将在堆栈为空时立即执行,而不是立即执行。如果执行类似 <code>setTimeout(fn, 0)</code> 之类的代码,之后立即运行从 1 到 100亿 的循环之后,回调将在几秒后执行。 </p>
</div>
<p>在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息 (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see it running live</a>, and <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-settimeout.html">see the source code</a>):</p>
<pre class="brush: js notranslate">let myGreeting = setTimeout(function() {
alert('Hello, Mr. Universe!');
}, 2000)</pre>
<p>我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给 <code>setTimeout()</code> 。以下两个版本的代码片段相当于第一个版本:</p>
<pre class="brush: js notranslate">// With a named function
let myGreeting = setTimeout(function sayHi() {
alert('Hello, Mr. Universe!');
}, 2000)
// With a function defined separately
function sayHi() {
alert('Hello Mr. Universe!');
}
let myGreeting = setTimeout(sayHi, 2000);</pre>
<p>例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时。</p>
<p><code>setTimeout()</code> 返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 <a href="#清除超时">清除超时</a>。</p>
<h3 id="传递参数给setTimeout">传递参数给setTimeout() </h3>
<p>我们希望传递给<code>setTimeout()</code>中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。</p>
<p>例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:</p>
<pre class="brush: js notranslate">function sayHi(who) {
alert('Hello ' + who + '!');
}</pre>
<p>人名可以通过第三个参数传进 <code>setTimeout()</code> :</p>
<pre class="brush: js notranslate">let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');</pre>
<h3 id="清除超时">清除超时</h3>
<p>最后,如果创建了 timeout,您可以通过调用<code>clearTimeout()</code>,将<code>setTimeout()</code>调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:</p>
<pre class="brush: js notranslate">clearTimeout(myGreeting);</pre>
<div class="blockIndicator note">
<p><strong>注意</strong>: 请参阅 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/greeter-app.html">greeter-app.html</a> 以获得稍微复杂一点的演示,该演示允许您在表单中设置要打招呼的人的姓名,并使用单独的按钮取消问候语(<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/greeter-app.html">see the source code also</a>)。</p>
</div>
<h2 id="setInterval">setInterval()</h2>
<p>当我们需要在一段时间之后运行一次代码时,<code>setTimeout()</code>可以很好地工作。但是当我们需要反复运行代码时会发生什么,例如在动画的情况下?</p>
<p>这就是<code>setInterval()</code>的作用所在。这与<code>setTimeout()</code>的工作方式非常相似,只是作为第一个参数传递给它的函数,<strong>重复</strong>执行的时间不少于第二个参数给出的毫秒数,<strong>而不是一次执行</strong>。您还可以将正在执行的函数所需的任何参数作为 <code>setInterval()</code> 调用的后续参数传递。</p>
<p>让我们看一个例子。下面的函数创建一个新的<code>Date()</code>对象,使用<code>toLocaleTimeString()</code>从中提取一个时间字符串,然后在UI中显示它。然后,我们使用<code>setInterval()</code>每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。</p>
<p>(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see this live</a>, and also <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">see the source</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><font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">像</span></font>setTimeout()一样</code>, <code>setInterval()</code> 返回一个确定的值,稍后你可以用它来取消间隔任务。</p>
<h3 id="清除intervals">清除intervals</h3>
<p><code>setInterval</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>话虽如此,我们还是要给你一个挑战。以我们的<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-clock.html">setInterval-clock.html</a>为例,修改它以创建您自己的简单秒表。</p>
<p>你要像前面一样显示时间,但是在这里,你需要:</p>
<ul>
<li>"Start" 按钮开始计时.</li>
<li>"Stop" 按钮暂停/停止计时</li>
<li>"Reset" 按钮清零.</li>
<li>时间显示经过的秒数.</li>
</ul>
<p>提示:</p>
<ul>
<li>您可以随心所欲地构造按钮标记;只需确保使用HTML语法,确保JavaScript获取按钮引用。</li>
<li>创建一个变量,从0开始,每秒钟增加1.</li>
<li>与我们版本中所做的一样,在不使用<code>Date</code>()对象的情况下创建这个示例更容易一些,但是不那么精确——您不能保证回调会在恰好1000ms之后触发。更准确的方法是运行<code>startTime = Date.now()</code>来获取用户单击start按钮的确切时间戳,然后执行<code>Date.now() - startTime</code>来获取单击<code>start</code>按钮后的毫秒数。</li>
<li>您还希望将小时、分钟和秒作为单独的值计算,然后在每次循环迭代之后将它们一起显示在一个字符串中。从第二个计数器,你可以计算出他们.</li>
<li>如何计时时分秒? 想一下:
<ul>
<li>一小时3600秒.</li>
<li>分钟应该是:时间减去小时后剩下的除以60.</li>
<li>分钟都减掉后,剩下的就是秒钟了.</li>
</ul>
</li>
<li>如果这个数字小于10,最好在显示的值前面加一个0,这样看起来更加像那么回事。</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/setinterval-stopwatch.html">see it runing live</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/setinterval-stopwatch.html">source code</a> also).</p>
</div>
<h2 id="关于_setTimeout_和_setInterval_需要注意的几点">关于 setTimeout() 和 setInterval() 需要注意的几点</h2>
<p>当使用 <code>setTimeout()</code> 和 <code>setInterval()</code>的时候,有几点需要额外注意。 现在让我们回顾一下:</p>
<h3 id="递归的timeouts">递归的timeouts</h3>
<p>还有另一种方法可以使用<code>setTimeout()</code>:我们可以递归调用它来重复运行相同的代码,而不是使用<code>setInterval()</code>。</p>
<p>下面的示例使用递归<code>setTimeout()</code>每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有何不同?">递归setTimeout()和setInterval()有何不同?</h4>
<p>上述代码的两个版本之间的差异是微妙的。</p>
<ul>
<li>递归 <code>setTimeout()</code> 保证执行之间的延迟相同,例如在上述情况下为100ms。 代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。</li>
<li>使用 <code>setInterval()</code> 的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 - 然后间隔最终只有60毫秒。</li>
<li>当递归使用 <code>setTimeout()</code> 时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。 换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。</li>
</ul>
<p>当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的 <code>setTimeout()</code> - 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。</p>
<h3 id="立即超时">立即超时</h3>
<p>使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。</p>
<p>举个例子,下面的代码(<a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/zero-settimeout.html">see it live</a>) 输出一个包含警报的"Hello",然后在您点击第一个警报的OK之后立即弹出“world”。</p>
<pre class="brush: js notranslate">setTimeout(function() {
alert('World');
}, 0);
alert('Hello');</pre>
<p><font><font>如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。</font></font></p>
<h3 id="使用_clearTimeout_or_clearInterval清除">使用 clearTimeout() or clearInterval()清除</h3>
<p><code>clearTimeout()</code> 和<code>clearInterval()</code> 都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除 <code>setTimeout()</code> 和 <code>setInterval()。</code></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> 遇到的问题创建的,比如 <code>setInterval()</code>并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。</p>
<p>(在<a href="http://creativejs.com/resources/requestanimationframe/index.html">CreativeJS</a>上了解有关此内容的更多信息).</p>
<div class="blockIndicator note">
<p><strong>注意:</strong> 你可以在课程中其他地方找到<code>requestAnimationFrame()</code> 的使用范例—参见 <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Drawing graphics</a>, 和 <a href="/en-US/docs/Learn/JavaScript/Objects/Object_building_practice">Object building practice</a>。</p>
</div>
<p><font>该方法将重新加载页面之前要调用的回调函数作为参数。</font><font>这是您将看到的常见表达:</font></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>注意</strong>: 如果要执行某种简单的常规DOM动画, <a href="/en-US/docs/Web/CSS/CSS_Animations">CSS 动画</a> 可能更快,因为它们是由浏览器的内部代码计算而不是JavaScript直接计算的。但是,如果您正在做一些更复杂的事情,并且涉及到在DOM中不能直接访问的对象(such as <a href="/en-US/docs/Web/API/Canvas_API">2D Canvas API</a> or <a href="/en-US/docs/Web/API/WebGL_API">WebGL</a> objects), <code>requestAnimationFrame()</code> 在大多数情况下是更好的选择。</p>
</div>
<h3 id="你的动画跑得有多快?">你的动画跑得有多快?</h3>
<p>动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps)为单位进行测量。这个数字越高,动画看起来就越平滑。</p>
<p>由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。</p>
<p>如果您有一个刷新率为60Hz的显示器,并且希望达到60fps,则大约有16.7毫秒(1000/60)来执行动画代码来渲染每个帧。这提醒我们,我们需要注意每次通过动画循环时要运行的代码量。</p>
<p><code>requestAnimationFrame()</code> 总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。<code>requestAnimationFrame()</code> 会尽其所能利用现有资源提升帧速率。</p>
<h3 id="requestAnimationFrame_与_setInterval_和_setTimeout有什么不同"> requestAnimationFrame() 与 setInterval() 和 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>需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。四舍五入是一个好主意,浏览器可能会尝试运行动画的速度超过60fps,它不会对动画的平滑度有任何影响。如前所述,60Hz是标准刷新率。</p>
<h3 id="包括时间戳">包括时间戳</h3>
<p>传递给 <code>requestAnimationFrame()</code> 函数的实际回调也可以被赋予一个参数(一个时间戳值),表示自 <code>requestAnimationFrame()</code> 开始运行以来的时间。这是很有用的,因为它允许您在特定的时间以恒定的速度运行,而不管您的设备有多快或多慢。您将使用的一般模式如下所示:</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>setInterval()<font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">或</span></font></code><code>setTimeout()</code> 相比最近的浏览器支持<code>requestAnimationFrame()</code></p>
<p>— <code>requestAnimationFrame()</code>.在Internet Explorer 10及更高版本中可用。因此,除非您的代码需要支持旧版本的IE,否则没有什么理由不使用<code>requestAnimationFrame()</code> 。</p>
<h3 id="一个简单的例子">一个简单的例子</h3>
<p>学习上述理论已经足够了,下面让我们仔细研究并构建自己的<code>requestAnimationFrame()</code> 示例。我们将创建一个简单的“旋转器动画”(spinner animation),即当应用程序忙于连接到服务器时可能会显示的那种动画。</p>
<div class="blockIndicator note">
<p><strong>注意</strong>: 一般来说,像以下例子中如此简单的动画应用CSS动画来实现,这里使用<code>requestAnimationFrame()</code>只是为了帮助解释其用法。<code>requestAnimationFrame()</code>正常应用于如逐帧更新游戏画面这样的复杂动画。</p>
</div>
<ol>
<li>
<p>首先, 下载我们的<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>的高度设置为100%<code><html></code>的高度,并将<code><div></code>水平和竖直居中。</p>
<pre class="brush: html 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>元素中。这里我们存储了一个<div>的引用在一个常量中,设置<code>rotateCount</code>变量为 0, 设置一个未初始化的变量之后将会用作容纳一个<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 rAF;
let startTime = null;</pre>
</li>
<li>
<p>在之前的代码下面, 插入一个 <code>draw()</code> 函数将被用作容纳我们的动画代码,并且包含了 <code>时间戳</code> 参数。</p>
<pre class="brush: js notranslate">function draw(timestamp) {
}</pre>
</li>
<li>
<p>在<code>draw()</code>中, 加入下述的几行。 如果起始时间还没有被赋值的话,将<code>timestamp</code> 传给它(这只将发生在循环中的第一步)。 并赋值给<code>rotateCount</code> ,以旋转 旋转器(spinning)(此处取<code>(当前时间戳 – 起始时间戳) / 3</code>,以免它转得太快):</p>
<pre class="brush: js notranslate"> if (!startTime) {
startTime = timestamp;
}
rotateCount = (timestamp - startTime) / 3;
</pre>
</li>
<li>
<p>在<code>draw()</code>内我们刚刚添加的代码之后,添加以下代码 — 此处是在检查<code>rotateCount</code> 的值是否超过了<code>359</code> (e.g. <code>360</code>, 一个正圆的度数)。 如果是,与360取模(值除以 <code>360</code> 后剩下的余数),使得圆圈的动画能以合理的低值连续播放。需要注意的是,这样的操作并不是必要的,只是比起类似于“128000度”的值,运行在 0-359 度之间会使你的操作更容易些。</p>
<pre class="brush: js notranslate">if (rotateCount > 359) {
rotateCount %= 360;
}</pre>
</li>
<li>
<p>接下来,在上一个块下面添加以下行以实际旋转 旋转(spinner)器:</p>
<pre class="notranslate">spinner.style.transform = `rotate(${rotateCount}deg)`;</pre>
</li>
<li>
<p>在函数<code>draw()</code>内的最下方,插入下面这行代码。这是整个操作中最关键的部分 — 我们将前面定义的变量设置为以<code>draw()</code>函数为参数的活动<code>requestAnimation()</code>调用。 这样动画就开始了,以尽可能接近60 FPS的速率不断地运行<code>draw()</code>函数。</p>
<pre class="notranslate">rAF = requestAnimationFrame(draw);</pre>
</li>
<li>
<p>在 <code>draw()</code> 函数定义下方,添加对 <code>draw()</code> 函数的调用以启动动画。</p>
</li>
<li>
<pre class="notranslate">draw();</pre>
</li>
</ol>
<div class="blockIndicator note">
<p><strong>Note</strong>: You can find this <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">example live on GitHub</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/simple-raf-spinner.html">source code</a> also).</p>
</div>
<h3 id="撤销requestAnimationFrame">撤销requestAnimationFrame()</h3>
<p><code>requestAnimationFrame()</code>可用与之对应的<code>cancelAnimationFrame()</code>方法“撤销”(不同于“set…”类方法的“清除”,此处更接近“撤销”之意)。</p>
<p>该方法以<code>requestAnimationFrame()</code>的返回值为参数,此处我们将该返回值存在变量 <code>rAF</code> 中:</p>
<pre class="brush: js notranslate">cancelAnimationFrame(rAF);</pre>
<h3 id="主动学习:_启动和停止旋转器(spinner)">主动学习: 启动和停止旋转器(spinner)</h3>
<p>在这个练习中,我们希望你能对<code>cancelAnimationFrame()</code>方法做一些测试,在之前的示例中添加新内容。你需要在示例中添加一个事件监听器,用于当鼠标在页面任意位置点击时,启动或停止旋转器。 </p>
<p>一些提示:</p>
<ul>
<li>包含<code><body></code>在内大多数元素都可以添加<code>click</code>事件处理器<font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff;">。为了使可点击区域最大化,直接监听</span></font><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动画">限制 (节流) requestAnimationFrame()动画</h3>
<p>requestAnimationFrame() 的限制之一是无法选择帧率。在大多数情况下,这不是问题,因为通常希望动画尽可能流畅地运行。但是,当要创建老式的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帧。如果通过 <code>requestAnimationFrame()</code> 为屏幕上显示的每个帧显示不同的精灵帧,则 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 帧,因为我们将每帧更新 <code>posX</code> 2个单位值(角色在屏幕上的位置)</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>对于本文的最后部分,将创建一个 2 人反应游戏。游戏将有两个玩家,其中一个使用 <kbd>A</kbd> 键控制游戏,另一个使用 <kbd>L</kbd> 键控制游戏。</p>
<p>按下 <em>开始</em> 按钮后,像前面看到的旋转器那样的将显示 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">starter file for the app</a> 。其中包含完成的 HTML 结构和 CSS 样式,为我们提供了一个游戏板,其中显示了两个玩家的信息(如上所示),但 spinner 和 结果段落重叠显示,只需要编写 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>开始时间为null。当旋转器开始旋转时,它将赋值为 开始时间。</li>
<li>一个未初始化的变量,用于之后存储使 旋转器 动画化的 {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}} 调用。</li>
<li>开始按钮的引用。</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>接下来添加 <code>draw()</code> 函数以使 旋转器 动画化。这与之前的简单旋转器示例中的版本非常相似:</p>
<pre class="brush: js notranslate"> function draw(timestamp) {
if(!startTime) {
startTime = timestamp;
}
let 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>好的,准备充分!现在该使游戏变得可玩了!将以下代码块添加到代码中。 <code>start()</code> 函数调用 <code>draw()</code> 以启动 旋转器,并在UI中显示它,隐藏“开始”按钮,这样您就无法通过同时启动多次来弄乱游戏,并运行一个经过5到10秒的随机间隔后,会运行 <code>setEndgame()</code> 函数的 <code>setTimeout()</code> 。下面的代码块还将一个事件侦听器添加到按钮上,以在单击它时运行 <code>start()</code> 函数。
<pre class="notranslate">btn.addEventListener('click', start);
function start() {
draw();
spinnerContainer.style.display = 'block';
btn.style.display = 'none';
setTimeout(setEndgame, random(5000,10000));
}</pre>
</li>
<li>添加以下方法到代码:</li>
</ol>
<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) {
console.log(e.key);
if(e.key === 'a') {
result.textContent = 'Player 1 won!!';
} else if(e.key === 'l') {
result.textContent = 'Player 2 won!!';
}
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> 事件侦听器附加到 document。当按下任何按钮时,<code>keyHandler()</code> 函数将运行。</li>
<li>在 <code>keyHandler()</code> 里,在 <code>keyHandler()</code> 内部,代码包括作为参数的事件对象作为参数(用 <code>e</code> 表示)- 其 {{domxref("KeyboardEvent.key", "key")}} 属性包含刚刚按下的键,可以通过这个对象来对特定的操作和特定的按键做出响应。</li>
<li>将变量 <code>isOver</code> 设置为 <code>false</code> ,这样我们就可以跟踪是否按下了正确的按键以使玩家1或2获胜。我们不希望游戏在按下错误的键后结束。</li>
<li>将 <code>e.key</code> 输出到控制台,这是找出所按的不同键的键值的有用方法。</li>
<li>当 <code>e.key</code> 为“ a”时,显示一条消息说玩家1获胜;当 <code>e.key</code> 为“ l”时,显示消息说玩家2获胜。 (注意:这仅适用于小写的a和l - 如果提交了大写的 A 或 L(键加上 <kbd>Shift</kbd>),则将其视为另一个键!)如果按下了其中一个键,请将 <code>isOver</code> 设置为 <code>true</code> 。</li>
<li>仅当 <code>isOver</code> 为 <code>true</code> 时,才使用 {{domxref("EventTarget.removeEventListener", "removeEventListener()")}} 删除 <code>keydown</code> 事件侦听器,以便一旦产生获胜的按键,就不再有键盘输入可以弄乱最终游戏结果。您还可以使用 <code>setTimeout()</code> 在5秒钟后调用 <code>reset()</code>-如前所述,此函数将游戏重置为原始状态,以便可以开始新游戏。</li>
</ol>
<p>就这样-一切都完成了!</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: 如果卡住了, check out <a href="https://mdn.github.io/learning-area/javascript/asynchronous/loops-and-intervals/reaction-game.html">our version of the reaction game</a> (see the <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/loops-and-intervals/reaction-game.html">source code</a> also).</p>
</div>
<h2 id="结论">结论</h2>
<p>就是这样-异步循环和间隔的所有要点在一篇文章中介绍了。您会发现这些方法在许多情况下都很有用,但请注意不要过度使用它们!因为它们仍然在主线程上运行,所以繁重的回调(尤其是那些操纵DOM的回调)会在不注意的情况下降低页面的速度。</p>
<p>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous")}}</p>
<h2 id="In_this_module">In this module</h2>
<ul>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Concepts">General asynchronous programming concepts</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">Introducing asynchronous JavaScript</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">Cooperative asynchronous JavaScript: Timeouts and intervals</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">Making asynchronous programming easier with async and await</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">Choosing the right approach</a></li>
</ul>
|