深入理解JavaScript定时机制与事件循环

作者:窗边听雨时间:2026-10-30 15:51:04浏览:563

本文深入解析JavaScript单线程运行机制,阐述浏览器多线程协作与主线程执行栈的关系。详细剖析同步与异步任务在任务队列中的排队逻辑,以及事件循环如何调度回调函数。重点对比setTimeout与setInterval在定时器线程中的行为差异,通过代码示例说明时间间隔的误差来源及间歇调用的连续执行现象,帮助开发者准确掌握JS异步编程核心原理。

JavaScript运行机制概述

本文旨在解析JavaScript的定时机制,理解这一机制的前提是掌握JavaScript的整体运行原理。

首先明确一个核心概念:JavaScript引擎线程是单线程运行的,且采用事件驱动模型。

浏览器多线程架构

一、浏览器中的多线程架构

现代浏览器的内核通常包含以下基本线程:

1、JavaScript引擎线程。

2、定时器线程,负责处理setInterval和setTimeout触发的计时任务。

3、浏览器事件触发线程,负责响应onclick、onmousemove等用户交互事件。

4、界面渲染线程,负责HTML元素的可视化渲染。需要注意的是,当JavaScript引擎线程正在执行脚本时,界面渲染线程会处于挂起状态。这意味着对DOM节点的修改不会立即反映在界面上,而是需等待JavaScript引擎线程空闲后才会重新渲染(这一点将在后文结合示例说明)。

5、HTTP请求线程,Ajax请求也在此线程中运行。

上述线程在浏览器内核的统一调度下协同工作,具体内部实现细节较为复杂。

任务队列与执行栈

二、任务队列机制

由于JavaScript是单线程语言,所有代码均在JavaScript引擎线程(也称为主线程)中执行,这构成了一个执行栈。以下总结主要基于阮一峰老师的相关文章理解。

JavaScript代码可视为一系列任务,分为同步任务和异步任务。同步任务(如变量赋值、alert调用、函数声明等)直接在主线程上按顺序执行;异步任务(如浏览器事件触发、Ajax服务器响应等)则根据时间顺序进入任务队列(也称事件队列或消息队列)排队。只有当主线程上的同步任务执行完毕,系统才会检查任务队列,将排队的任务移入主线程执行。

以下通过代码示例进行说明:


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>定时机制</title>

<style type="text/css">
body{
  margin: 0;
  padding: 0;
  position: relative;
  height: 600px;
}

#test{
  height: 30px;
  width: 30px;
  position: absolute;
  left: 0;
  top: 100px;
  background-color: pink;
}
</style>
</head>
<body>
  <div id="test">
  
  </div>

<script>
  var pro = document.getElementById('test');
  pro.onclick = function() {
    alert('我没有立即被执行。');
  };
  function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
  }

 test();
</script>
</body>
</html>

在该示例中,test()函数的执行耗时约8至9秒。因此,在页面加载后的8秒内点击粉色方块,提示框不会立即弹出,而是需等待8秒后才会显示。此外,8秒内点击粉色方块的次数,决定了8秒后提示框弹出的次数。

页面加载时,主线程首先声明test函数,接着声明变量pro,将p节点赋值给pro,并为p节点绑定click事件及其回调函数(此时回调函数处于挂起状态),最后调用test函数执行其内部代码。在test函数执行期间,若用户点击p节点,浏览器事件触发线程检测到该事件,便将其放入任务队列,等待主线程中的test函数执行完毕后,检查任务队列并执行相应的回调函数。若多次点击,这些事件将按触发时间的先后顺序在任务队列中排队(可通过为其他元素添加点击事件并交替点击来验证)。

以下是任务运行机制的总结:

异步任务的运行机制如下(同步任务可视为没有异步任务的异步执行,原理相同):

1、所有同步任务均在主线程上执行,形成执行栈(execution context stack)。

2、主线程之外存在一个“任务队列”(task queue)。当异步任务产生运行结果时,会在任务队列中放置一个对应事件。

3、一旦“执行栈”中的所有同步任务执行完毕,系统会读取“任务队列”,检查其中存在的事件。对应的异步任务结束等待状态,进入执行栈开始执行。

4、主线程不断重复上述第三步,形成循环。

事件与回调函数处理

三、事件与回调函数

在为DOM元素指定事件时,通常需指定一个回调函数,以便事件发生时执行相应代码。

主线程中事件的回调函数会被挂起。如果任务队列中有正在排队的相应事件,当主线程检测到该事件时,便会执行对应的回调函数。因此,主线程执行异步任务的过程,实质上就是执行相应的回调函数。

事件循环模型解析

四、事件循环模型

主线程检查任务队列中事件的过程是持续循环的,因此可以绘制事件循环图:

JavaScript运行机制概述

上图中,主线程生成堆和执行栈。栈中任务执行完毕后,主线程检查任务队列中由其他线程传入的事件,定位排在最前面的事件,从挂起的回调函数中找出与该事件对应的回调函数,并在执行栈中执行。此过程不断重复。

定时器原理与差异对比

五、定时器原理分析

结合上述知识,下面深入探讨JavaScript中的定时器:setTimeout()和setInterval()。

setTimeout(func, t)用于超时调用,即在间隔一段时间后调用函数。其在事件循环中的过程如下(个人理解):

主线程执行完setTimeout(func, t)语句后,将回调函数func挂起,同时定时器线程开始计时。当计时时间等于t时,相当于发生了一个事件,该事件被传入任务队列(此时计时结束,仅计时一次)。当主线程中的其他任务执行完毕后,检查任务队列发现该事件,便执行挂起的回调函数func。这里指定的时间间隔t仅为参考值,实际时间间隔取决于执行setTimeout(func, t)语句后其他代码所花费的时间,且实际间隔总是大于或等于t(即使将t设为0,也需经历此过程)。

setInterval(func, t)用于间歇调用,即每隔一段时间调用一次函数。其过程与上述类似,但存在关键差异。

setTimeout()是在经过时间t后,由定时器线程在任务队列中添加一个事件(注意仅添加一个);而setInterval()则是每经过时间t(持续计时,除非清除间歇调用)后,定时器线程便在任务队列中添加一个事件,无论之前添加的事件是否已被主线程检测并执行。(实际上浏览器具有智能处理机制:处理setInterval时,若发现任务队列中已存在同一ID的间歇调用事件,则会直接丢弃新来的事件。即任务队列中同一时刻只能存在一个来自同一ID的间歇调用事件。)

举例说明:假设执行完setInterval(func, t)后的代码耗时2t。当2t时间过去后,主线程从任务队列中检测到定时器线程传入的第一个间歇调用事件,func开始执行。当第一次func执行完毕后,第二次的间歇调用事件早已传入任务队列,主线程立即检测到该事件,func函数随即再次执行。在这种情况下,func的两次执行是连续发生的,中间没有时间间隔。

以下是一个具体示例:


function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
 }
  function test2() {
   var d = new Date().valueOf();
   //var e = d-a;
   console.log('我被调用的时刻是:'+d+'ms');
   //alert(1);
  }
  setInterval(test2,3000);
  
 test();

运行结果:

JavaScript运行机制概述

为何在8.6秒后没有输出两个相同的时刻?原因可在前文内容中找到。

示例中的for循环耗时8601ms。在执行for循环期间,任务队列中仅有一个间歇调用事件在排队(原因如上所述)。当8601ms结束后,第一个间歇调用事件进入主线程。对于此例,此时任务队列已空,可再次传入间歇调用事件,因此时刻1477462632228ms的第二次间歇调用事件(实为第三次)传入任务队列。由于主线程执行栈已空,主线程立即执行对应的回调函数。第二次调用与第一次调用之间仅间隔320ms(即8601+320=8920,约等于9秒)。我们看到第三次调用已恢复正常,因为此时主线程中无其他代码,仅有一个任务,即隔一段时间执行一次间歇调用的回调函数。

使用setTimeout()实现间歇调用的示例:


function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
  }
 
  function test2(){
   var d = new Date().valueOf();
   console.log('我被调用的时刻是:'+d+'ms');
   setTimeout(test2,3000);
  }
  setTimeout(test2,3000);
 test();

运行结果:

JavaScript运行机制概述

每两次调用的时间间隔基本相同。请思考其原因。

再看一个示例:


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flex布局练习</title>

<style type="text/css">
body{
  margin: 0;
  padding: 0;
  position: relative;
  height: 600px;
}

#test{
  height: 30px;
  width: 30px;
  position: absolute;
  left: 0;
  top: 100px;
  background-color: pink;
}
</style>
</head>
<body>
  <div id="test">
  
  </div>

<script>
 var p = document.createElement('p');
 p.style.width = '50px';
 p.style.height = '50px';
 p.style.border = '1px solid black';
 
 document.body.appendChild(p);

 alert('ok');
 
</script>
</body>
</html>

该示例的结果是提示框先弹出,随后黑色边框的p元素才出现在页面中。原因很简单,归结为一句话:

在JavaScript引擎运行脚本期间,界面渲染线程处于挂起状态。也就是说,当使用JavaScript操作界面节点时,修改不会立即体现,需等待JavaScript引擎线程空闲时才会反映出来。

以上是对JavaScript定时机制的理解与总结,如有错误,恳请指正。也希望大家多多支持本站。

作者最新文章