JavaScript 定时器
1.导读
在写 setTimeout 和 setInterval 代码时,你是否有想过一下几点:
- 他们是怎么实现的?
- 面试时如果问你原理怎么回答?
- 为什么要了解定时器原理?
首先 setTimeout 和 setInterval 都不是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(函数节流)函数
函数节流解释:对函数执行增加一个控制层,保证一段时间内(可配置)内只执行一次。此函数的作用是对函数执行进行频率控制,常用于用户频繁触发但可以以更低频率响应的场景

其中一种解决办法:
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)
}
}
谢谢阅读, 欢迎大家继续补充