Nodejs中JavaScript的计时器的工作原理

Nodejs中JavaScript的计时器的工作原理

最近都在看一些JavaScript原理层面的文章,恰巧看到了jQuery的作者的一篇关于JavaScript计时器原理的解析,于是诚惶诚恐地决定把原文翻译成中文,一来是为了和大家分享,二来是为了加深自己对于JavaScript的理解。 原文链接:http://ejohn.org/blog/how-javascript-timers-work/ 博客原文链接: http://www.cnblogs.com/yuanzm/p/4126762.html 原文翻译: 从基础层面来讲,理解JavaScript计时器的工作原理是很重要的。由于JavaScript是单线程的,所以很多时候计时器并不是表现得和我们的直观想象一样。让我们从下面的三个函数开始,它们能够让我们有机会去构造和操作计时器。 (1)var id =setTimeout(fn, delay); -创建了一个简单的计时器,在经过给定的时间后,回调函数将会被执行。这个函数会返回一个唯一的ID,便于在之后某个时间可以注销这个计时器。 (2)var id = setInterval(fn, delay); -和setTimeout类似,但是每经过一段时间(给定的延时),所传递的函数就会被执行一次,直到这个定时器被注销。 (3)clearInterval(id);, clearTimeout(id); -接受一个计时器ID(由之前两种计时器返回)并且停止计时器回调函数的执行。 为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没有保证的。因为所有在浏览器中执行的JavaScript单线程异步事件(比如鼠标点击事件和计时器)都只有在它有空的时候才执行。这最好通过图片来说明,就如下面这张图所示: Timers.png 这一张图片里面有很多信息需要慢慢消化,但是彻底地理解这张图片将会让你对JavaScript异步执行是如何工作的有一个更好的认识。这张图片是从一维的角度来阐述的:在垂直方向是以毫秒计的时间,蓝色的块代表了

当前正在执行的JavaScript代码段。比如第一段JavaScript执行了大概18毫秒,鼠标点击事件大概执行了11毫秒。

由于JavaScript每次只能执行一段代码(基于它单线程的特性),所以所有这些代码段都阻塞了其他异步事件的执行。这就意味着,当一件异步事件(比如鼠标点击,计时器触发和一个XMLHttpRequest 请求完成)触发的时候,这些事件的回调函数将排在执行队列的最后去等待执行(排队的方式因浏览器不同而不同,这里只是一个简化)。

一开始,在第一段代码段内,两个计时器被初始化:一个10ms的setTimeout 和一个10ms的setInterval。由于计时器在哪儿初始化就在那儿开始计时,所以实际上计时器在第一段代码执行完成之前就触发了。然而,计时器的回调函数并不是立即执行了(单线程限制了不能这样做),相反的是,回调函数排在了执行队列的最后,等到下一个有空的时间去执行。

此外,在第一个代码块内我们看到了一个鼠标点击事件发生了。与之相关的javascript异步事件(我们不可能预测用户会在什么时候去采取这样的动作,因此这个事件被视为异步的)并不会立即执行。和计时器一样的是,它被放到了队列的最后去等待执行。 在第一个代码快执行完成的时候,浏览器会立即发出这样的询问:谁正在等待执行?这个时候,鼠标点击处理程序和计时器回调函数都在等待执行。浏览器选择了其中一个(鼠标点击回调函数)并且立即执行它。为了执行,计时器会等到下一个可能执行的时间。

我们注意到,当鼠标点击事件对应的处理程序正在执行的时候,第一个定时回调函数也要执行了。同定时计时器一样,它也在队列的后面等待执行。然而,我们可以注意到,当定时器再一次触发(在计时器回调函数正在执行的时候),这一次定时器回调函数被丢弃了。如果在执行一大块代码块的时候,你把所有的定时回调函数都放在队列的最后,结果就是一大串定时回调函数将会没有间隔的一起执行,直到完成。相反,在把更多定时回调函数放到队列之前,浏览器会静静的等待,知道队列中的所有定时回调函数都执行完成。

事实上,我们可以看到,当interval回调函数正在执行的时候,interval第三次被触发。这给我们一个很重要的信息:interval并不关心当前谁在执行,它的回调函数会不加区分地进入队列,即使存在这个回调函数会被丢弃的可能。

最后,当第二个定时回调函数完成执行的时候,我们可以看到javascript引擎已经没有什么需要执行了。这意味着,浏览器现在正在等待一个新的异步事件的发生。我们可以看到在50ms的时候,定时回调函数再一次被触发。然而,这一次,没有其他代码阻塞他的执行了,所以他立即执行了定时回调函数。

让我们看一个例子来更好地阐述setTimeout 和setInterval的区别。 setTimeout(function(){ /* Some long block of code… / setTimeout(arguments.callee, 10); }, 10); setInterval(function(){ / Some long block of code… */ }, 10);

第一眼看上去这两段代码在功能上是等价的,但事实上却不是。值得注意的是,setTimeout 这段代码会在每次回调函数执行之后至少需要延时10ms再去执行一次(可能是更多,但是不会少)。但是setInterval会每隔10ms就去尝试执行一次回调函数,不管上一个回调函数是不是还在执行。

从这里我们能够学到很多,让我们来概括一下:

javascript引擎只有一个线程,迫使异步事件只能加入队列去等待执行。 在执行异步代码的时候,setTimeout 和setInterval 是有着本质区别的。 如果计时器被正在执行的代码阻塞了,它将会进入队列的尾部去等待执行直到下一次可能执行的时间出现(可能超过设定的延时时间)。 如果interval回调函数执行需要花很长时间的话(比指定的延时长),interval有可能没有延迟背靠背地执行。 上述这一切对于理解js引擎是如果工作的无疑是很重要的知识,尤其是大量的典型的异步事件发生时,对于构建一个高效的应用代码片段来说是一个非常有利的基础。 个人见解: 翻译完成之后,感觉对于javascript异步有了新的认识,但是可能初学者看不太懂这篇文章,于是写了一个demo,运行在nodejs环境下(浏览器不容易模拟) var startTime = new Date();

//初始化计时器 var start = setTimeout(function() { var end = new Date(); console.log(‘10ms的计时器执行完成,距离程序开始’ + (end - start) + ‘ms’); }, 10);

//模拟鼠标点击事件 function asyncReal(data, callback) { process.nextTick(function() { callback();
}); } var asyncStart = new Date(); asyncReal(‘yuanzm’, function() { var asyncEnd = new Date(); console.log(‘模拟鼠标执行事件完成,花费时间’ + (asyncEnd - asyncStart) + ‘ms’); })

//设定定时器 count = 1; var interval = setInterval(function() { ++count; if(count === 5) { clearInterval(interval); } console.log(‘定时器事件’); },10);

//模拟第一阶段代码执行 var first = []; var start = new Date(); for(var i = 0;i < 10000000;i++){ first.push(i); } var end = new Date(); console.log(‘第一阶段代码执行完成,用时’ + (end - start) + ‘ms’); 272118240745444.jpg 我们按照文中的原理来解释一下: (1) 一开始设定的计时器并不是在10ms后立即执行,而是被添加到了队列后面,等到第一阶段代码执行完成才执行,距离开始的时间也不是设定的10ms (2)鼠标点击事件同样因为是异步事件,添加到了队列后面,等到第一阶段代码执行完成的时候才执行。 (3)鼠标点击事件先于计时器事件添加到队列后面 (4)最后定时器才能执行


5 回复

Node.js 中 JavaScript 的计时器的工作原理

引言

理解 JavaScript 计时器的工作原理对于开发者来说非常重要,尤其是在 Node.js 环境中。尽管 JavaScript 是单线程的,但计时器机制允许我们在特定时间后执行代码。本文将深入探讨 setTimeoutsetInterval 的工作原理,并通过示例代码帮助你更好地理解。

计时器的基本概念

在 Node.js 中,你可以使用以下几种方式来设置计时器:

  1. setTimeout(fn, delay) - 创建一个简单的计时器,在经过指定的时间后,回调函数将会被执行。该函数返回一个唯一的 ID,以便稍后取消计时器。
  2. setInterval(fn, delay) - 类似于 setTimeout,但它会周期性地执行回调函数,直到计时器被取消。
  3. clearTimeout(id)clearInterval(id) - 接受一个计时器 ID 并停止计时器的执行。

计时器的内部工作原理

计时器的延时并不是绝对准确的,因为所有 JavaScript 代码都是单线程的。这意味着所有异步事件(如计时器、鼠标点击等)都必须等待当前正在执行的代码块完成后才能执行。

示例代码

// 初始化计时器
var startTime = new Date();

setTimeout(function() {
    var endTime = new Date();
    console.log('10ms 的计时器执行完成,距离程序开始 ' + (endTime - startTime) + ' ms');
}, 10);

// 模拟鼠标点击事件
function asyncReal(data, callback) {
    process.nextTick(function() {
        callback();
    });
}

var asyncStartTime = new Date();
asyncReal('yuanzm', function() {
    var asyncEndTime = new Date();
    console.log('模拟鼠标执行事件完成,花费时间 ' + (asyncEndTime - asyncStartTime) + ' ms');
});

// 设定定时器
let count = 1;
const interval = setInterval(function() {
    ++count;
    if (count === 5) {
        clearInterval(interval);
    }
    console.log('定时器事件');
}, 10);

// 模拟第一阶段代码执行
var first = [];
var start = new Date();
for (let i = 0; i < 10000000; i++) {
    first.push(i);
}
var end = new Date();
console.log('第一阶段代码执行完成, 用时 ' + (end - start) + ' ms');

解释

  1. 计时器的延时不是绝对准确的 - 在上面的代码中,setTimeout 设置为 10ms,但实际上它会在第一阶段代码执行完成后才执行。
  2. 异步事件的顺序 - 鼠标点击事件和计时器事件都会被添加到执行队列中,但由于鼠标点击事件先被添加到队列中,所以它会先于计时器事件执行。
  3. setInterval 的行为 - setInterval 会周期性地执行回调函数,即使前一个回调函数还没有完成执行。如果前一个回调函数执行时间较长,setInterval 可能会出现背靠背的执行。

总结

  • JavaScript 引擎只有一个线程,因此所有异步事件都必须等待当前代码块执行完毕。
  • setTimeoutsetInterval 在执行时会有不同的行为,setTimeout 会在回调函数执行完毕后再执行,而 setInterval 则会周期性地尝试执行回调函数。
  • 理解这些基本原理对于编写高效的 Node.js 应用非常重要。

通过以上示例代码和解释,希望你能更清楚地理解 Node.js 中 JavaScript 计时器的工作原理。


希望大家能够给我的demo提一些建议

代码可以用 Markdown 标记一下

嗯嗯,标记一下

Node.js 中 JavaScript 计时器的工作原理

JavaScript 计时器(如 setTimeoutsetInterval)的工作原理基于单线程的特性,这意味着所有任务必须按顺序执行。计时器并不保证在设定的时间后立即执行,而是将其回调函数放入事件队列中,等待主线程可用时再执行。

示例代码

// 初始化计时器
var startTime = new Date();
var startTimer = setTimeout(() => {
    var endTime = new Date();
    console.log(`10ms 的计时器执行完成,距离程序开始 ${endTime - startTime} ms`);
}, 10);

// 模拟鼠标点击事件
function simulateMouseEvent(callback) {
    process.nextTick(callback);
}

var mouseClickTime = new Date();
simulateMouseEvent(() => {
    var mouseClickEndTime = new Date();
    console.log(`模拟鼠标点击事件完成,花费时间 ${mouseClickEndTime - mouseClickTime} ms`);
});

// 设定定时器
let count = 0;
const interval = setInterval(() => {
    count++;
    if (count === 5) {
        clearInterval(interval);
    }
    console.log('定时器事件');
}, 10);

// 模拟第一阶段代码执行
var firstPhaseStartTime = new Date();
for (let i = 0; i < 10000000; i++) {
    // 做一些计算
}
var firstPhaseEndTime = new Date();
console.log(`第一阶段代码执行完成,用时 ${firstPhaseEndTime - firstPhaseStartTime} ms`);

解释

  1. 计时器 (setTimeout): 在设定的时间(如 10ms)后,计时器的回调函数会被放入事件队列中。但由于主线程在处理其他任务(例如循环),计时器的实际执行时间可能会晚于设定的时间。

  2. 鼠标点击事件 (simulateMouseEvent): 这个事件同样被放入事件队列中,只有在主线程空闲时才会执行。

  3. 定时器 (setInterval): 每隔设定的时间(如 10ms)都会将回调函数放入事件队列中。如果前一个回调函数还在执行,则下一个回调函数也会被加入队列。

  4. 主线程: 主线程按顺序处理任务,只有在处理完当前任务后才会处理队列中的下一个任务。这就是为什么实际的执行时间可能会晚于设定的时间。

通过这段代码和解释,你可以更好地理解 JavaScript 计时器在 Node.js 中的工作原理。

回到顶部