从零开始再学 JavaScript 定时器

JavaScript 定时器

1.导读

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

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

首先 setTimeoutsetInterval 都不是ECMAScript规范或者任何JavaScript实现的一部分。它是由浏览器实现,而且在不一样的浏览器也会有所差别。定时器也能够由 Nodejs 运行时自己实现。前端

在浏览器中,定时器是 Window 对象下的 api,因此能够直接在控制台进行直接调用。java

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

有些人确定会想,为何必定要了解这些糟糕无聊的原理,咱们只须要运用别人 api 进行开发不就能够了。很遗憾的告诉你,做为一名 JavaScript 开发人员,我认为若是你只是想一直作一个初级开发工程师,那么你能够不去了解,若是想要提高,若是不去了解,那可能代表你并不彻底理解V8(和其余虚拟机)如何与浏览器和Node交互。git

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

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那样是内联函数。 这是不使用内联函数的相同示例:api

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”bash

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

复制代码

谢谢阅读, 欢迎你们继续补充

参考文献

相关文章
相关标签/搜索