阅读 886

从零开始再学 JavaScript 定时器

JavaScript 定时器

1.导读

在写 setTimeoutsetInterval 代码时,你是否有想过一下几点:

  • 他们是怎么实现的?
  • 面试时如果问你原理怎么回答?
  • 为什么要了解定时器原理?

首先 setTimeoutsetInterval 都不是ECMAScript规范或者任何JavaScript实现的一部分。它是由浏览器实现,并且在不同的浏览器也会有所差异。定时器也可以由 Nodejs 运行时本身实现。

在浏览器中,定时器是 Window 对象下的 api,所以可以直接在控制台进行直接调用。

Nodejs 中,定时器是 global 对象的一部分,这点和浏览器的 Window 类似。具体可以去查看下node-timers源码

有些人肯定会想,为什么一定要了解这些糟糕无聊的原理,我们只需要运用别人 api 进行开发不就可以了。很遗憾的告诉你,作为一名 JavaScript 开发人员,我认为如果你只是想一直做一个初级开发工程师,那么你可以不去了解,如果想要提升,如果不去了解,那可能表明你并不完全理解V8(和其他虚拟机)如何与浏览器和Node交互。

本文会通过案例来讲解 JavaScript 定时器,还会讲解某条的一些面试题

2.定时器的一些案例

2.1 延迟案例

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

复制代码

上面这个例子用 setTimeout 延时 4 秒打印问候语。 如果你在node环境执行 example1.js。Node将会暂停4秒然后打印问候语(接着退出)。

  • setTimeout 第一个参数function - 是你想要在到期时间(delay毫秒)之后执行的函数。

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

const func = () => {
  console.log('Hello after 4 seconds');
};
setTimeout(func, 4 * 1000);
复制代码
  • setTimeout 第二个参数 delay - 延迟的毫秒数 (一秒等于1000毫秒),函数的调用会在该延迟之后发生。如果省略该参数,delay取默认值0,意味着“马上”执行,或者尽快执行。不管是哪种情况,实际的延迟时间可能会比期待的(delay毫秒数) 值长
  • setTimeout 第三个参数 param1, ..., paramN 可选 附加参数,一旦定时器到期,它们会作为参数传递给 function
/ 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”

2.2 案例2

使用您到目前为止学到的关于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 调用也得到一个 第三个 参数来表示theOneFunc的delay 参数。

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

2.3 setInterval 案例

如果要求你每隔 4秒 打印一条消息怎么办? 虽然你可以将setTimeout放在一个循环中,但定时器API也提供了setInterval函数,这将完成永远做某事的要求。

// example3.js
setInterval(
  () => console.log('Hello every 4 seconds'),
  4000
);
复制代码

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

2.4 清除定时器

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调用。

在前面的例子中,您是否注意到在“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循环同步阻止运行时。 1e10是1后面有10个零,所以循环是一个10个十亿滴答循环(基本上模拟繁忙的CPU)。 当此循环正在滴答时,节点无法执行任何操作。

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

推荐大家看一篇Node.js Event loop 原理 里面讲的很深。

2.4 打印脚本并推出进程

编写脚本每秒打印消息“ 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。

2.5 this 和定时器结合时

当你在常规函数中使用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标识)

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

//  What will this print??
setTimeout(obj.whoCalledMe, 0);
复制代码

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

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

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

2.6 连续打印具有不同延迟的消息“Hello World”

以1秒的延迟开始,然后每次将延迟增加1秒。 第二次将延迟2秒。 第三次将延迟3秒,依此类推。

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

Hello World. 1
Hello World. 2
Hello World. 3...
复制代码

【注意】你只能使用const来定义变量。 你不能使用 let 或 var。 我们先进行分析如下:

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

以下是解决问题的一种方法:

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

编写一个脚本以连续打印消息“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 语句。

以下是一种解决办法

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

复制代码

3.面试中的定时器

3.1 某条 - 使用 JS 实现一个 repeat 方法

使用 JS 实现一个 repeat 方法,输入输出如下:
// 实现
function repeat (func, times, wait) {},
// 输入
const repeatFunc = repeat(alert, 4, 3000);
// 输出
调用这个 repeatedFunc ("hellworld"),会 alert4 次 helloworld, 每次间隔 3 秒
复制代码

某一种解决办法如下

function repeat(func, times, wait) {
    return function () {       
        let timer = null
        const args = arguments
        let i = 0;
        timer = setInterval(()=>{
            while (i >= times) {
                clearInterval(timer)
                return
            } 
            i++
            func.apply(null, args)
        }, wait)
    }
 
}
复制代码

3.2 某条-请用 JS 实现 throttle(函数节流)函数

函数节流解释:对函数执行增加一个控制层,保证一段时间内(可配置)内只执行一次。此函数的作用是对函数执行进行频率控制,常用于用户频繁触发但可以以更低频率响应的场景

如上图,在一段时间内函数触发了 9 次,实际只执行了 5 次,且每次执行的时间间隔不小于 100ms;

其中一种解决办法:

function debounce (fn, time) {
   let first = true
   let timer = null
    return function (...args) {
        if (first) {
           first = false
            fn.apply(this, args)
            
        }
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, 100)
    }
}

复制代码

谢谢阅读, 欢迎大家继续补充

参考文献

关注下面的标签,发现更多相似文章
评论