1. 首页

JavaScript定时器:你需要知道的每个点

几周前我在推特上发布了这样一个面试问题:

JavaScript面试问题:

在哪里可以找到setTimeout和setInterval的源代码?(他们在哪里实现的?)

你怎么在面试中回答?(你不能去网上搜索)

function setTimeOut(callback

继续往下看之前先试着回答这个问题


推特上半数的回答都是错误的 回答不是 V8 (或者其他虚拟机!!)尽管著名的“JavaScript定时器”函数像setTimeoutsetInterval都不是ECMAScript规范或者任何JavaScript实现的一部分。 定时器功能由浏览器实现,它们的实现在不同浏览器之间会有所不同。定时器也可以由Node.js运行时本身实现。

在浏览器里主要的定时器函数是作为Window对象的接口,Window对象同时拥有很多其他方法和对象。该接口使其所有元素在JavaScript全局作用域中都可用。这就是为什么你可以直接在浏览器控制台执行setTimeout

在node里,定时器是global对象的一部分,这点很像浏览器中的Window。你可以在Node里看到定时器的源码 这里.

有些人可能认为这是一个糟糕的面试问题 – 为什么一定要知道这个问题呢?!作为一名JavaScript开发人员,我认为你应该知道这一点,因为如果你不这样做,那可能表明你并不完全理解V8(和其他虚拟机)如何与浏览器和Node交互。

让我们开始做一些关于定时器函数的例子和挑战把?


更新: 这篇文章现在是“完整介绍Node.js”的一部分。 你可以在这里阅读最新的版本。


Delaying the execution of a function

定时器函数是高阶函数,可用于延迟或重复执行其他函数(它们作为第一个参数接收)。

这是一个关于延迟的例子:

    // example1.js
    setTimeout(
      () => {
        console.log('Hello after 4 seconds');
      },
      4 * 1000
    );

这个例子用setTimeout 延时4秒打印问候语。setTimeout的第二个参数是延时(多少毫秒)。这就是为什么我用4*1000来表示4秒

setTimeout的第一个参数是一个将被延迟执行的函数

如果你在node环境执行 example1.js。Node将会暂停4秒然后打印问候语(接着退出)。

请注意,setTimeout的第一个参数只是一个函数引用。 它不必像example1.js那样是内联函数。 这是不使用内联函数的相同示例:

    const func = () => {
      console.log('Hello after 4 seconds');
    };
    setTimeout(func, 4 * 1000);

Passing Arguments

如果使用setTimeout延迟的函数需要携带参数,我们可以把参数放在setTimeout里(放在已知的两个参数后)来中转参数给需要延迟执行的函数。

    // For: func(arg1, arg2, arg3, ...)
    // We can use: setTimeout(func, delay, arg1, arg2, arg3, ...)

举个例子:

    // example2.js
    const rocks = who => {
      console.log(who + ' rocks');
    };
    setTimeout(rocks, 2 * 1000, 'Node.js');

上面的rocks延迟2秒执行,接收who参数并且通过setTimeout中转字符串“Node.js”给函数的who参数。

node环境执行example2.js控制台会在2秒后打印“Node.js rocks

Timers Challenge #1

使用您到目前为止学到的关于setTimeout的知识,在相应的延迟后打印以下2条消息。

  • 4秒后打印消息“Hello after 4 seconds

  • 8秒后打印“Hello after 8 seconds”消息。

约束:您只能在解决方案中定义一个函数,其中包括内联函数。 这意味着许多setTimeout调用必须使用完全相同的函数。

解决方案

以下是我如何解决这一挑战:

    // solution1.js
    const theOneFunc = delay => {
      console.log('Hello after ' + delay + ' seconds');
    };
    setTimeout(theOneFunc, 4 * 1000, 4);
    setTimeout(theOneFunc, 8 * 1000, 8);

我让theOneFunc收到一个delay参数,并在打印的消息中使用了delay参数的值。 这样,该函数可以根据我们传递给它的任何延迟值打印不同的消息。

然后我在两次setTimeout的调用中使用了theOneFunc,一个在4秒后触发,另一个在8秒后触发。 这两个setTimeout调用也得到一个 第三个 参数来表示theOneFuncdelay参数。

使用node命令执行solution1.js文件将打印出挑战要求的内容,4秒后的第一条消息和8秒后的第二条消息。

Repeating the execution of a function

如果我要求你每隔4秒打印一条消息怎么办?

虽然你可以将setTimeout放在一个循环中,但定时器API也提供了setInterval函数,这将完成永远做某事的要求。

这是setInterval的一个例子:

    // example3.js
    setInterval(
      () => console.log('Hello every 3 seconds'),
      3000
    );

此示例将每3秒打印一次消息。 使用node命令执行example3.js将使Node永远打印此消息,直到你终止该进程(使用CTRL + C)。

Cancelling Timers

因为调用计时器函数会调度操作,所以在执行之前也可以取消该操作。

setTimeout的调用返回一个定时器“ID”,你可以使用带有clearTimeout调用的定时器ID来取消该定时器。 下面是这个例子:

    // example4.js
    const timerId = setTimeout(
      () => console.log('You will not see this one!'),
      0
    );
    clearTimeout(timerId);

这个简单的计时器应该在“0”ms之后触发(使其立即生效),但它不会因为我们正在捕获timerId值并在使用clearTimeout调用后立即取消它。

当我们用node命令执行example4.js时,Node不会打印任何东西,进程就会退出。

顺便说一句,在Node.js中,还有另一种方法可以使用0 ms来执行setTimeout。 Node.js计时器API有另一个名为setImmediate的函数,它与setTimeout基本相同,带有0 ms但我们不必在那里指定延迟:

    setImmediate(
      () => console.log('I am equivalent to setTimeout with 0 ms'),
    );

setImmediate方法_在所有浏览器里都不支持。不要在前端代码里使用它。_

就像clearTimeout一样,还有一个clearInterval函数,它对于setInerval调用执行相同的操作,并且还有一个clearImmediate调用。

A timer delay is not a guaranteed thing

在前面的例子中,您是否注意到在“0”ms之后执行带有setTimeout的内容并不意味着立即执行它(在setTimeout行之后),而是在脚本中的所有其他内容之后立即执行它(包括clearTimeout调用)?

让我用一个例子清楚地说明这一点。 这是一个简单的setTimeout调用,应该在半秒后触发,但它不会:

    // example5.js
    setTimeout(
      () => console.log('Hello after 0.5 seconds. MAYBE!'),
      500,
    );
    for (let i = 0; i < 1e10; i++) {
      // Block Things Synchronously
    }

在此示例中定义计时器之后,我们使用大的for循环同步阻止运行时。 1e101后面有10个零,所以循环是一个10个十亿滴答循环(基本上模拟繁忙的CPU)。 当此循环正在滴答时,节点无法执行任何操作。

这当然是在实践中做的非常糟糕的事情,但它会帮助你理解setTimeout延迟不是一个保证的东西,而是一个最小的东西。 500ms表示最小延迟为500ms。 实际上,脚本将花费更长的时间来打印其问候语。 它必须等待阻塞循环才能完成。

Timers Challenge #2

编写脚本每秒打印消息“ Hello World ”,但只打印5次。 5次之后,脚本应该打印消息“Done”并让节点进程退出。

约束:你不能使用setTimeout调用来完成这个挑战。 提示:你需要一个计数器。

解决方案

以下是我如何解决这个问题:

    let counter = 0;
    const intervalId = setInterval(() => {
      console.log('Hello World');
      counter += 1;
    if (counter === 5) {
        console.log('Done');
        clearInterval(intervalId);
      }
    }, 1000);

我将counter值作为0启动,然后启动一个setInterval调用同时捕获它的id。

延迟功能将打印消息并每次递增计数器。 在延迟函数内部,if语句将检查我们现在是否处于5次。 如果是这样,它将打印“Done”并使用捕获的intervalId常量清除间隔。 间隔延迟为“1000”ms。

Who exactly “calls” the delayed functions?

当你在常规函数中使用JavaScript的this关键字时,如下所示:

    function whoCalledMe() {
      console.log('Caller is', this);
    }

this关键字内的值将代表函数的调用者。 如果在Node REPL中定义上面的函数,则调用者将是global对象。 如果在浏览器的控制台中定义函数,则调用者将是window对象。

让我们将函数定义为对象的属性,以使其更清晰:

    const obj = {
      id: '42',
      whoCalledMe() {
        console.log('Caller is', this);
      }
    };
    // The function reference is now: obj.whoCallMe

现在当你直接使用它的引用调用obj.whoCallMe函数时,调用者将是obj对象(由其id标识):

JS中文网是中国领先的新一代开发者社区和专业的技术媒体

现在,问题是,如果我们将obj.whoCallMe的引用传递给setTimetout调用,调用者会是什么?

    //  What will this print??
    setTimeout(obj.whoCalledMe, 0);

在这种情况下调用者会是谁?

答案根据执行计时器功能的位置而有所不同。 在这种情况下,你根本无法取决于调用者是谁。 你失去了对调用者的控制权,因为定时器实现将是现在调用您的函数的实现。 如果你在Node REPL中测试它,你会得到一个Timetout对象作为调用者:

JS中文网

请注意,这只在您在常规函数中使用JavaScript的this关键字时才有意义。 如果您使用箭头函数,则根本不需要担心调用者。

Timers Challenge #3

编写脚本以连续打印具有不同延迟的消息“Hello World”。 以1秒的延迟开始,然后每次将延迟增加1秒。 第二次将延迟2秒。 第三次将延迟3秒,依此类推。

在打印的消息中包含延迟时间。 预期输出看起来像:

    Hello World. 1
    Hello World. 2
    Hello World. 3...

约束:你只能使用const来定义变量。 你不能使用letvar

解决方案

因为延迟量是这个挑战中的一个变量,我们不能在这里使用setInterval,但我们可以在递归调用中使用setTimeout手动创建一个间隔执行。 使用setTimeout的第一个执行函数将创建另一个计时器,依此类推。

另外,因为我们不能使用let / var,所以我们不能有一个计数器来增加每个递归调用的延迟时间,但我们可以使用递归函数参数在递归调用期间递增。

这是解决这一挑战的一种可能方法:

    const greeting = delay =>
      setTimeout(() => {
        console.log('Hello World. ' + delay);
        greeting(delay + 1);
      }, delay * 1000);
    greeting(1);

Timers Challenge #4

编写一个脚本以连续打印消息“Hello World”,其具有与挑战#3相同的变化延迟概念,但这次是每个主延迟间隔的5个消息组。 从前5个消息的延迟100ms开始,接下来的5个消息延迟200ms,然后是300ms,依此类推。

以下是代码的要求:

  • 在100ms点,脚本将开始打印“Hello World”,并以100ms的间隔进行5次。 第一条消息将出现在100毫秒,第二条消息将出现在200毫秒,依此类推。

  • 在前5条消息之后,脚本应将主延迟增加到200ms。 因此,第6条消息将在500毫秒+ 200毫秒(700毫秒)打印,第7条消息将在900毫秒打印,第8条消息将在1100毫秒打印,依此类推。

  • 在10条消息之后,脚本应将主延迟增加到300毫秒。 所以第11条消息应该在500ms + 1000ms + 300ms(18000ms)打印。 第12条消息应打印在21000ms,依此类推。

  • 一直重复上面的模式。

在打印的消息中包含延迟。 预期的输出看起来像这样(没有注释):

    Hello World. 100  // At 100ms
    Hello World. 100  // At 200ms
    Hello World. 100  // At 300ms
    Hello World. 100  // At 400ms
    Hello World. 100  // At 500ms
    Hello World. 200  // At 700ms
    Hello World. 200  // At 900ms
    Hello World. 200  // At 1100ms...

约束:您只能使用setInterval调用(而不是setTimeout),并且只能使用一个if语句。

解决方案

因为我们只能使用setInterval调用,所以我们还需要递归来增加下一个setInterval调用的延迟。 另外,我们需要一个if语句来控制只有在5次调用该递归函数之后才能执行此操作。

以下是其中一种解决方案:

    let lastIntervalId, counter = 5;
    const greeting = delay => {
      if (counter === 5) {
        clearInterval(lastIntervalId);
        lastIntervalId = setInterval(() => {
          console.log('Hello World. ', delay);
          greeting(delay + 100);
        }, delay);
        counter = 0;
      }
    counter += 1;
    };
    greeting(100);

end

原文地址:medium.com

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:JavaScript定时器:你需要知道的每个点

链接:https://www.javascriptc.com/1902.html

« React工作面试—招聘面试官的观点
码农程序员月薪12000的北京真实生活»
Flutter 中文教程资源

相关推荐

QR code