合作异步JavaScript: 超时和间隔

在这里,我们将讨论传统的JavaScript方法,这些方法可以在一段时间或一段规则间隔(例如,每秒固定的次数)之后,以异步方式运行代码,并讨论它们的用处,以及它们的固有问题。

预备条件: 基本的计算机知识,对JavaScript基本原理有较好的理解。
目标: 了解异步循环和间隔及其用途。

介绍

很长一段时间以来,web平台为JavaScript程序员提供了许多函数,这些函数允许您在一段时间间隔过后异步执行代码,或者重复异步执行代码块,直到您告诉它停止为止。这些都是:

setTimeout()
在指定的时间后执行一段代码.
setInterval()
以固定的时间间隔,重复运行一段代码.
requestAnimationFrame()
setInterval()的现代版本;在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行.

这些函数设置的异步代码实际上在主线程上运行(在其指定的计时器过去之后)。

在 setTimeout() 调用执行之前或 setInterval() 迭代之间可以(并且经常会)运行其他代码。根据这些操作的处理器密集程度,它们可以进一步延迟异步代码,因为任何异步代码仅在主线程可用后才执行(换句话说,当调用栈为空时)。在阅读本文时,您将学到更多关于此问题的信息。

无论如何,这些函数用于在web站点或应用程序上运行不间断的动画和其他后台处理。在下面的部分中,我们将向您展示如何使用它们。

setTimeout()

正如前述, setTimeout() 在指定的时间后执行一段特定代码. 它需要如下参数:

  • 要运行的函数,或者函数引用。
  • 表示在执行代码之前等待的时间间隔(以毫秒为单位,所以1000等于1秒)的数字。如果指定值为0(或完全省略该值),函数将尽快运行(参阅下面的注释,了解为什么它“尽快”而不是“立即”运行)。稍后详述这样做的原因。
  • 更多的参数:在指定函数运行时,希望传递给函数的值。

Note: 指定的时间(或延迟)不能保证在指定的确切时间之后执行,而是最短的延迟执行时间。在主线程上的堆栈为空之前,传递给这些函数的回调将无法运行。

结果,像 setTimeout(fn, 0) 这样的代码将在堆栈为空时立即执行,而不是立即执行。如果执行类似 setTimeout(fn, 0) 之类的代码,之后立即运行从 1 到 100亿 的循环之后,回调将在几秒后执行。 

在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息 (see it running live, and see the source code):

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

我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给 setTimeout() 。以下两个版本的代码片段相当于第一个版本:

// 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);

例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时。

setTimeout() 返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 Clearing timeouts

传递参数给setTimeout() 

我们希望传递给setTimeout()中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。

例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:

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

人名可以通过第三个参数传进 setTimeout()

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

清除超时

最后,如果创建了 timeout,您可以通过调用clearTimeout(),将setTimeout()调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:

clearTimeout(myGreeting);

注意: 请参阅 greeter-app.html 以获得稍微复杂一点的演示,该演示允许您在表单中设置要打招呼的人的姓名,并使用单独的按钮取消问候语(see the source code also)。

setInterval()

当我们需要在一段时间之后运行一次代码时,setTimeout()可以很好地工作。但是当我们需要反复运行代码时会发生什么,例如在动画的情况下?

这就是setInterval()的作用所在。这与setTimeout()的工作方式非常相似,只是作为第一个参数传递给它的函数,重复执行的时间不少于第二个参数给出的毫秒数,而不是一次执行。您还可以将正在执行的函数所需的任何参数作为 setInterval() 调用的后续参数传递。

让我们看一个例子。下面的函数创建一个新的Date()对象,使用toLocaleTimeString()从中提取一个时间字符串,然后在UI中显示它。然后,我们使用setInterval()每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。

(see this live, and also see the source):

function displayTime() {
   let date = new Date();
   let time = date.toLocaleTimeString();
   document.getElementById('demo').textContent = time;
}

const createClock = setInterval(displayTime, 1000);

setTimeout()一样, setInterval() 返回一个确定的值,稍后你可以用它来取消间隔任务。

清除intervals

setInterval()永远保持运行任务,除非我们做点什么——我们可能会想阻止这样的任务,否则当浏览器无法完成任何进一步的任务时我们可能得到错误, 或者动画处理已经完成了。我们可以用与停止超时相同的方法来实现这一点——通过将setInterval()调用返回的标识符传递给clearInterval()函数:

const myInterval = setInterval(myFunction, 2000);

clearInterval(myInterval);

主动学习:创建秒表!

话虽如此,我们还是要给你一个挑战。以我们的setInterval-clock.html为例,修改它以创建您自己的简单秒表。

你要像前面一样显示时间,但是在这里,你需要:

  • "Start" 按钮开始计时.
  • "Stop" 按钮暂停/停止计时
  • "Reset" 按钮清零.
  • 时间显示经过的秒数.

提示:

  • 您可以随心所欲地构造按钮标记;只需确保使用HTML语法,确保JavaScript获取按钮引用。
  • 创建一个变量,从0开始,每秒钟增加1.
  • 与我们版本中所做的一样,在不使用Date()对象的情况下创建这个示例更容易一些,但是不那么精确——您不能保证回调会在恰好1000ms之后触发。更准确的方法是运行startTime = Date.now()来获取用户单击start按钮的确切时间戳,然后执行Date.now() - startTime来获取单击start按钮后的毫秒数。
  • 您还希望将小时、分钟和秒作为单独的值计算,然后在每次循环迭代之后将它们一起显示在一个字符串中。从第二个计数器,你可以计算出他们.
  • 如何计时时分秒? 想一下:
    • 一小时3600秒.
    • 分钟应该是:时间减去小时后剩下的除以60.
    • 分钟都减掉后,剩下的就是秒钟了.
  • 如果这个数字小于10,最好在显示的值前面加一个0,这样看起来更加像那么回事。
  • 暂停的话,要清掉间隔函数。重置的话,显示要清零,要更新显示

Note: 如果您在操作过程有困难,请参考 see it runing live (see the source code also).

关于 setTimeout() 和 setInterval() 需要注意的几点

当使用 setTimeout()setInterval()的时候,有几点需要额外注意。 现在让我们回顾一下:

递归的timeouts

还有另一种方法可以使用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() 保证执行之间的延迟相同,例如在上述情况下为100ms。 代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。
  • 使用 setInterval() 的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 - 然后间隔最终只有60毫秒。
  • 当递归使用 setTimeout() 时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。 换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。

当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的 setTimeout() - 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。

立即超时

使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。

举个例子,下面的代码(see it live) 输出一个包含警报的"Hello",然后在您点击第一个警报的OK之后立即弹出“world”。

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

alert('Hello');

如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。

使用 clearTimeout() or clearInterval()清除

clearTimeout()clearInterval() 都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除 setTimeout() 和 setInterval()。

但为了保持一致性,你应该使用 clearTimeout() 来清除 setTimeout() 条目,使用 clearInterval() 来清除 setInterval() 条目。 这样有助于避免混乱。

requestAnimationFrame()

requestAnimationFrame() 是一个专门的循环函数,旨在浏览器中高效运行动画。它基本上是现代版本的setInterval() —— 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行,不管其运行的环境如何。

它是针对setInterval() 遇到的问题创建的,比如 setInterval()并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。

(在CreativeJS上了解有关此内容的更多信息).

注意: 你可以在课程中其他地方找到requestAnimationFrame() 的使用范例—参见 Drawing graphics, 和 Object building practice

该方法将重新加载页面之前要调用的回调函数作为参数。这是您将看到的常见表达:

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

draw();

这个想法是要定义一个函数,在其中更新动画 (例如,移动精灵,更新乐谱,刷新数据等),然后调用它来开始这个过程。在函数的末尾,以 requestAnimationFrame() 传递的函数作为参数进行调用,这指示浏览器在下一次显示重新绘制时再次调用该函数。然后这个操作连续运行, 因为requestAnimationFrame() 是递归调用的。

注意: 如果要执行某种简单的常规DOM动画, CSS 动画 可能更快,因为它们是由浏览器的内部代码计算而不是JavaScript直接计算的。但是,如果您正在做一些更复杂的事情,并且涉及到在DOM中不能直接访问的对象(such as 2D Canvas API or WebGL objects), requestAnimationFrame() 在大多数情况下是更好的选择。

你的动画跑得有多快?

动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps)为单位进行测量。这个数字越高,动画看起来就越平滑。

由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。

如果您有一个刷新率为60Hz的显示器,并且希望达到60fps,则大约有16.7毫秒(1000/60)来执行动画代码来渲染每个帧。这提醒我们,我们需要注意每次通过动画循环时要运行的代码量。

requestAnimationFrame() 总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。requestAnimationFrame() 会尽其所能利用现有资源提升帧速率。

 requestAnimationFrame() 与 setInterval() 和 setTimeout()有什么不同?

让我们进一步讨论一下 requestAnimationFrame() 方法与前面介绍的其他方法的区别. 下面让我们看一下代码:

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

draw();

现在让我们看看如何使用setInterval():

function draw() {
   // Drawing code goes here
}

setInterval(draw, 17);

如前所述,我们没有为requestAnimationFrame();指定时间间隔;它只是在当前条件下尽可能快速平稳地运行它。如果动画由于某些原因而处于屏幕外浏览器也不会浪费时间运行它。

 另一方面setInterval()需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。四舍五入是一个好主意,浏览器可能会尝试运行动画的速度超过60fps,它不会对动画的平滑度有任何影响。如前所述,60Hz是标准刷新率。

包括时间戳

传递给 requestAnimationFrame() 函数的实际回调也可以被赋予一个参数(一个时间戳值),表示自 requestAnimationFrame() 开始运行以来的时间。这是很有用的,因为它允许您在特定的时间以恒定的速度运行,而不管您的设备有多快或多慢。您将使用的一般模式如下所示:

let startTime = null;

function draw(timestamp) {
    if(!startTime) {
      startTime = timestamp;
    }

   currentTime = timestamp - startTime;

   // Do something based on current time

   requestAnimationFrame(draw);
}

draw();

浏览器支持

 与setInterval()setTimeout() 相比最近的浏览器支持requestAnimationFrame()

requestAnimationFrame().在Internet Explorer 10及更高版本中可用。因此,除非您的代码需要支持旧版本的IE,否则没有什么理由不使用requestAnimationFrame() 。

一个简单的例子

学习上述理论已经足够了,下面让我们仔细研究并构建自己的requestAnimationFrame() 示例。我们将创建一个简单的“旋转器动画”(spinner animation),即当应用程序忙于连接到服务器时可能会显示的那种动画。

注意: 一般来说,像以下例子中如此简单的动画应用CSS动画来实现,这里使用requestAnimationFrame()只是为了帮助解释其用法。requestAnimationFrame()正常应用于如逐帧更新游戏画面这样的复杂动画。

  1. 首先, 下载我们的网页模板

  2. 放置一个空的 <div> 元素进入 <body>, 然后在其中加入一个 ↻ 字符.这是一个将循环的字符将在我们的例子中作为我们的旋转器(spinner)。

  3. 用任何你喜欢的方法应用下述的CSS到HTML模板中。这些在页面上设置了一个红色背景,将<body>的高度设置为100%<html>的高度,并将<div>水平和竖直居中。

    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;
    }
  4. 插入一个 <script>元素在 </body> 标签之上。

  5. 插入下述的JavaScript在你的 <script> 元素中。这里我们存储了一个<div>的引用在一个常量中,设置rotateCount变量为 0, 设置一个未初始化的变量之后将会用作容纳一个requestAnimationFrame() 的调用, 然后设置一个 startTime 变量为 null,它之后将会用作存储 requestAnimationFrame() 的起始时间.。

    const spinner = document.querySelector('div');
    let rotateCount = 0;
    let rAF;
    let startTime = null;
  6. 在之前的代码下面, 插入一个 draw() 函数将被用作容纳我们的动画代码,并且包含了 时间戳 参数。

    function draw(timestamp) {
    
    }
  7. draw()中, 加入下述的几行。 如果起始时间还没有被赋值的话,将timestamp 传给它(这只将发生在循环中的第一步)。 并赋值给rotateCount ,以旋转 旋转器(spinning)(此处取(当前时间戳 – 起始时间戳) / 3,以免它转得太快):

      if (!startTime) {
       startTime = timestamp;
      }
    
      rotateCount = (timestamp - startTime) / 3;
    
  8. draw()内我们刚刚添加的代码之后,添加以下代码 — 此处是在检查rotateCount 的值是否超过了359 (e.g. 360, 一个正圆的度数)。 如果是,与360取模(值除以 360 后剩下的余数),使得圆圈的动画能以合理的低值连续播放。需要注意的是,这样的操作并不是必要的,只是比起类似于“128000度”的值,运行在 0-359 度之间会使你的操作更容易些。

    if (rotateCount > 359) {
      rotateCount %= 360;
    }
  9. 接下来,在上一个块下面添加以下行以实际旋转 旋转(spinner)器:

    spinner.style.transform = `rotate(${rotateCount}deg)`;
  10. 在函数draw()内的最下方,插入下面这行代码。这是整个操作中最关键的部分 — 我们将前面定义的变量设置为以draw()函数为参数的活动requestAnimation()调用。 这样动画就开始了,以尽可能接近60 FPS的速率不断地运行draw()函数。

    rAF = requestAnimationFrame(draw);
  11. draw() 函数定义下方,添加对 draw() 函数的调用以启动动画。

  12. draw();

Note: You can find this example live on GitHub (see the source code also).

撤销requestAnimationFrame()

requestAnimationFrame()可用与之对应的cancelAnimationFrame()方法“撤销”(不同于“set…”类方法的“清除”,此处更接近“撤销”之意)。

该方法以requestAnimationFrame()的返回值为参数,此处我们将该返回值存在变量 rAF 中:

cancelAnimationFrame(rAF);

主动学习: 启动和停止旋转器(spinner)

在这个练习中,我们希望你能对cancelAnimationFrame()方法做一些测试,在之前的示例中添加新内容。你需要在示例中添加一个事件监听器,用于当鼠标在页面任意位置点击时,启动或停止旋转器。 

一些提示:

  • 包含<body>在内大多数元素都可以添加click事件处理器。为了使可点击区域最大化,直接监听<body>元素的事件是更有效的,因为事件可以冒泡到其子元素。
  • 你可能需要添加一个追踪用变量来检测旋转器是否在旋转。该变量若为真,则清除动画帧;若为假,则重新调用函数。

Note: 先自己尝试一下,如果你确实遇到困难,可以参看我们的在线示例源代码

限制 (节流) requestAnimationFrame()动画

requestAnimationFrame() 的限制之一是无法选择帧率。在大多数情况下,这不是问题,因为通常希望动画尽可能流畅地运行。但是,当要创建老式的8位类型的动画时,怎么办?

例如,在我们的 Drawing Graphics 文章中的猴子岛行走动画中的一个问题:

在此示例中,必须为角色在屏幕上的位置及显示的精灵设置动画。精灵动画中只有6帧。如果通过 requestAnimationFrame() 为屏幕上显示的每个帧显示不同的精灵帧,则 Guybrush 的四肢移动太快,动画看起来很荒谬。因此,此示例使用以下代码限制精灵循环的帧率:

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

因此,代码每13个动画帧循环一次精灵。

...实际上,大约是每 6.5 帧,因为我们将每帧更新 posX 2个单位值(角色在屏幕上的位置)

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

这是用于计算如何更新每个动画帧中的位置的代码。

用于限制动画的方法将取决于特定代码。例如,在前面的旋转器实例中,可以通过仅在每帧中增加 rotateCount 一个单位(而不是两个单位)来使其运动速度变慢。

主动学习:反应游戏

对于本文的最后部分,将创建一个 2 人反应游戏。游戏将有两个玩家,其中一个使用 A 键控制游戏,另一个使用 L 键控制游戏。

按下 开始 按钮后,像前面看到的旋转器那样的将显示 5 到 10 秒之间的随机时间。之后将出现一条消息,提示 “PLAYERS GO!!”。一旦这发生,第一个按下其控制按钮的玩家将赢得比赛。

让我们来完成以下工作:

  1. 首先,下载 starter file for the app 。其中包含完成的 HTML 结构和 CSS 样式,为我们提供了一个游戏板,其中显示了两个玩家的信息(如上所示),但 spinner 和 结果段落重叠显示,只需要编写 JavaScript 代码。

  2. 在页面上空的 <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. 包含旋转器 <div> 元素的引用。用于显示和隐藏它。
    3. 旋转计数。这确定了显示在动画每一帧上的旋转器旋转了多少。
    4. 开始时间为null。当旋转器开始旋转时,它将赋值为 开始时间。
    5. 一个未初始化的变量,用于之后存储使 旋转器 动画化的  requestAnimationFrame()  调用。
    6. 开始按钮的引用。
    7. 结果字段的引用。
  3. 接下来,在前几行代码下方,添加以下函数。它只接收两个数字并返回一个在两个数字之间的随机数。稍后将需要它来生成随机超时间隔。

    function random(min,max) {
      var num = Math.floor(Math.random()*(max-min)) + min;
      return num;
    }
  4. 接下来添加 draw() 函数以使 旋转器 动画化。这与之前的简单旋转器示例中的版本非常相似:

      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);
      }
  5. 现在是时候在页面首次加载时设置应用程序的初始状态了。添加以下两行,它们使用 display: none; 隐藏结果段落和旋转器容器。

    result.style.display = 'none';
    spinnerContainer.style.display = 'none';
  6. 接下来,定义一个 reset() 函数。该函数在游戏结束后将游戏设置回初始状态以便再次开启游戏。在代码底部添加以下内容:

    function reset() {
      btn.style.display = 'block';
      result.textContent = '';
      result.style.display = 'none';
    }
  7. 好的,准备充分!现在该使游戏变得可玩了!将以下代码块添加到代码中。 start() 函数调用 draw() 以启动 旋转器,并在UI中显示它,隐藏“开始”按钮,这样您就无法通过同时启动多次来弄乱游戏,并运行一个经过5到10秒的随机间隔后,会运行 setEndgame() 函数的 setTimeout() 。下面的代码块还将一个事件侦听器添加到按钮上,以在单击它时运行 start() 函数。
    btn.addEventListener('click', start);
    
    function start() {
      draw();
      spinnerContainer.style.display = 'block';
      btn.style.display = 'none';
      setTimeout(setEndgame, random(5000,10000));
    }
  8. 添加以下方法到代码:
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);
  };
}

逐步执行以下操作:

  1. 首先通过 cancelAnimationFrame()  取消 旋转器 动画(清理不必要的流程总是一件好事),隐藏 旋转器 容器。
  2. 接下来,显示结果段落并将其文本内容设置为“ PLAYERS GO !!”。向玩家发出信号,表示他们现在可以按下按钮来取胜。
  3. keydown 事件侦听器附加到 document。当按下任何按钮时,keyHandler() 函数将运行。
  4. 在 keyHandler() 里,在 keyHandler() 内部,代码包括作为参数的事件对象作为参数(用 e 表示)- 其 key 属性包含刚刚按下的键,可以通过这个对象来对特定的操作和特定的按键做出响应。
  5. 将变量 isOver 设置为 false ,这样我们就可以跟踪是否按下了正确的按键以使玩家1或2获胜。我们不希望游戏在按下错误的键后结束。
  6. e.key 输出到控制台,这是找出所按的不同键的键值的有用方法。
  7. e.key 为“ a”时,显示一条消息说玩家1获胜;当 e.key 为“ l”时,显示消息说玩家2获胜。 (注意:这仅适用于小写的a和l - 如果提交了大写的 A 或 L(键加上 Shift),则将其视为另一个键!)如果按下了其中一个键,请将 isOver 设置为 true
  8. 仅当 isOvertrue 时,才使用 removeEventListener() 删除 keydown 事件侦听器,以便一旦产生获胜的按键,就不再有键盘输入可以弄乱最终游戏结果。您还可以使用 setTimeout() 在5秒钟后调用 reset()-如前所述,此函数将游戏重置为原始状态,以便可以开始新游戏。

就这样-一切都完成了!

Note: 如果卡住了, check out our version of the reaction game (see the source code also).

结论

就是这样-异步循环和间隔的所有要点在一篇文章中介绍了。您会发现这些方法在许多情况下都很有用,但请注意不要过度使用它们!因为它们仍然在主线程上运行,所以繁重的回调(尤其是那些操纵DOM的回调)会在不注意的情况下降低页面的速度。

In this module