# JavaScript教程 - 15 定时器与事件循环

下面介绍一下定时器和 JavaScript 中的方法调用。

# 15.1 定时器

定时器可以延时周期性的执行函数。

# 1 setTimeout

setTimeout(callback, delay) 函数可以延时执行某个函数一次

举个栗子:

setTimeout(() => {
  alert("2 秒后执行一次");
}, 2000); // 单位是毫秒
1
2
3
  • 第一个参数是回调函数,延时时间到了以后要执行的函数;第二个参数是延迟时间(单位毫秒)。

如果要取消定时器,可以使用 clearTimeout(timer)。

举个栗子:

const timerId = setTimeout(() => {
  alert("2 秒后执行一次");
}, 2000); // 单位是毫秒

clearTimeout(timerId); // 取消执行
1
2
3
4
5
  • setTimeout() 会返回一个定时器的ID,可以通过 ID 使用 clearTimeout() 函数取消定时器。

# 2 setInterval

setInterval(callback, interval) 函数可以每隔一段时间重复执行函数(无限次,除非取消)。

举个栗子:

setInterval(() => {
  console.log("每隔 1 秒执行一次");
}, 1000);
1
2
3
  • 上面会每隔一秒执行一次回调函数。

可以使用 clearInterval 清除定时器,下面我们模拟一个倒计时:

let count = 5;
const timer = setInterval(() => {
  console.log(count);
  count--;
  if (count < 0) {
    clearInterval(timer);  // 停止计时器
    console.log("倒计时结束!");
  }
}, 1000);
1
2
3
4
5
6
7
8
9

# 15.2 方法调用

先查看下面的代码:

<script>  
	function c() {
    console.log('c');
    
    debugger;
  }

  function b() {
    console.log('b');
    c();
  }

  function a() {
    console.log('a');
    b();
  }

  a();
  
  console.log('代码执行完成!');
</script>  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 代码很简单,a() 函数调用 b() 函数,b() 函数调用 c() 函数。

当 JavaScript 程序开始执行时,首先会创建一个全局执行上下文,也就是下图的 全局栈帧 ,把它压入调用栈的最底部,全局栈帧中包含全局变量、全局函数等。

当调用 a() 函数,会将 a() 函数压入调用栈,当 a() 函数调用 b() 函数,会将 b() 函数也压入调用栈,依次类推,将 c() 函数也压入调用栈。栈帧中包含函数中定义的变量和函数声明等。

当函数调用完成,对应的栈帧会被弹出栈,如下图:

代码在执行的时候,我们可以使用浏览器的开发者工具,通过添加断点 debugger 来查看调用栈,如下图:

所以当全局作用域的代码被执行完毕后,调用栈是空的。也就是上面的代码 console.log('代码执行完成!') 执行完成后,调用栈为空。

# 15.3 事件循环与任务队列

JavaScript 是一种单线程语言,这意味着它一次只能做一件事。然而它可以执行异步操作,就像上面的定时器,但是回调函数并不是立即被压入到调用栈中执行的。

在定时器计时结束后,会将回调函数放入到 任务队列 中排队,等待执行,当调用栈为空时,才会将回调函数放入到调用栈中执行。

所以如果调用栈不为空,定时器时间到了,回调函数也不会被执行的。


举个栗子:

<script>
		setTimeout(() => {
      console.log("定时器结束");
    }, 2000);


    let startTime = Date.now();
    while (true) {
      if (Date.now() - startTime > 5000) {
        console.log("过了5秒");
        break;
      }
    }
    console.log("主程序执行完成");
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 在上面的代码中,首先定义了一个定时器,2秒后执行;
  • 然后主线程中定义了一个死循环,不停的获取当前时间和一开始的时间比较,如果过了5秒,则跳出循环。按理说在这个过程中,定时器倒计时应该结束了,执行回调函数,但是你会发现,回调函数是在主程序执行完成后调用的。

执行结果:

过了5秒
主程序执行完成
定时器结束
1
2
3

这是因为主程序没有执行完成,那么全局作用域的栈帧就一直在调用栈中,此时调用栈就不为空,回调函数就不会被压入到调用栈中执行,一直在事件队列中排队等待执行。主程序执行完成,调用栈为空,就立马将回调函数压入调用栈执行。