Node.js中使用计时器定时执行函数详解


Posted in Javascript onAugust 15, 2014

如果你熟悉客户端JavaScript编程,你可能使用过setTimeout和setInterval函数,这两个函数允许延时一段时间再运行函数。比如下面的代码, 一旦被加载到Web页面,1秒后会在页面文档后追加“Hello there”:

var oneSecond = 1000 * 1; // one second = 1000 x 1 ms
setTimeout(function() {
    document.write('<p>Hello there.</p>');
}, oneSecond);

而setInterval允许以指定的时间间隔重复执行函数。如果把下面的代码注入到Web页面,会导致每秒钟向页面文档后面追加一句“Hello there”:

                  var oneSecond = 1000 * 1; // one second = 1000 x 1 ms
                  setInterval(function() {
                                    document.write('<p>Hello there.</p>');
                  }, oneSecond);

因为Web早已成为一个用来构建应用程序的平台,而不再是简单的静态页面,所以这种类似的需求日益浮现。这些任务计划函数帮助开发人员实现表单定期验证,延迟远程数据同步,或者那些需要延时反应的UI交互。Node也完整实现了这些方法。在服务器端,你可以用它们来重复或延迟执行很多任务,比如缓存过期,连接池清理,会话过期,轮询等等。

使用setTimeout延迟函数执行

setTimeout可以制定一个在将来某个时间把指定函数运行一次的执行计划,比如:

                   var timeout_ms = 2000; // 2 seconds
                   var timeout = setTimeout(function() {
                            console.log("timed out!");
                   }, timeout_ms);

和客户端JavaScript完全一样,setTimeout接受两个参数,第一个参数是需要被延迟的函数,第二个参数是延迟时间(以毫秒为单位)。

setTimeout返回一个超时句柄,它是个内部对象,可以用它作为参数调用clearTimeout来取消计时器,除此之外这个句柄没有任何作用。

使用clearTimeout取消执行计划

一旦获得了超时句柄,就可以用clearTimeout来取消函数执行计划,像这样:

                   var timeoutTime = 1000; // one second
                   var timeout = setTimeout(function() {
                            console.log("timed out!");
                   }, timeoutTime);
                   clearTimeout(timeout);

 这个例子里,计时器永远不会被触发,也不会输出”time out!”这几个字。你也可以在将来的任何时间取消执行计划,就像下面的例子:

 var timeout = setTimeout(function A() {

 

                            console.log("timed out!");

 

                   }, 2000);

 

                   setTimeout(function B() {

 

                            clearTimeout(timeout);

 

                   }, 1000);

代码指定了两个延时执行的函数A和B,函数A计划在2秒钟后执行,B计划在1秒钟后执行,因为函数B先执行,而它取消了A的执行计划,因此A永远不会运行。

制定和取消函数的重复执行计划

setInterval和setTimeout类似,但是它会以指定时间为间隔重复执行一个函数。你可以用它来周期性的触发一段程序,来完成一些类似清理,收集,日志,获取数据,轮询等其它需要重复执行的任务。

下面代码每秒会向控制台输出一句“tick”:

                   var period = 1000; // 1 second
                   setInterval(function() {
                            console.log("tick");
                   }, period);

如果你不想让它永远运行下去,可以用clearInterval()取消定时器。

setInterval返回一个执行计划句柄,可以把它用作clearInterval的参数来取消执行计划:

                   var interval = setInterval(function() {
                            console.log("tick");
                   }, 1000);
                   // …
                   clearInterval(interval);

使用process.nextTick将函数执行延迟到事件循环的下一轮

有时候客户端JavaScript程序员用setTimeout(callback,0)将任务延迟一段很短的时间,第二个参数是0毫秒,它告诉JavaScript运行时,当所有挂起的事件处理完毕后立刻执行这个回调函数。有时候这种技术被用来延迟执行一些并不需要被立刻执行的操作。比如,有时候需要在用户事件处理完毕后再开始播放动画或者做一些其它的计算。

Node中,就像 “事件循环”的字面意思,事件循环运行在一个处理事件队列的循环里,事件循环工作过程中的每一轮就称为一个tick。

你可以在事件循环每次开始下一轮(下一个tick)执行时调用回调函数一次,这也正是process.nextTick的原理,而setTimeout,setTimeout使用JavaScript运行时内部的执行队列,而不是使用事件循环。

通过使用process.nextTick(callback) ,而不是setTimeout(callback, 0),你的回调函数会在队列内的事件处理完毕后立刻执行,它要比JavaScript的超时队列快很多(以CPU时间来衡量)。

你可以像下面这样,把函数延迟到下一轮事件循环再运行:

                   process.nextTick(function() {
                            my_expensive_computation_function();
                   });

  注意:process对象是Node为数不多的全局对象之一。

堵塞事件循环

Node和JavaScript的运行时采用的是单线程事件循环,每次循环,运行时通过调用相关回调函数来处理队列内的下个事件。当事件执行完毕,事件循环取得执行结果并处理下个事件,如此反复,直到事件队列为空。如果其中一个回调函数运行时占用了很长时间,事件循环在那期间就不能处理其它挂起的事件,这会让应用程序或服务变得非常慢。

在处理事件时,如果使用了内存敏感或者处理器敏感的函数,会导致事件循环变得缓慢,而且造成大量事件堆积,不能被及时处理,甚至堵塞队列。

看下面堵塞事件循环的例子:

                   process.nextTick(function nextTick1() {
                            var a = 0;
                            while(true) {
                                     a ++;
                            }
                   });
                   process.nextTick(function nextTick2() {
                            console.log("next tick");
                   });
                   setTimeout(function timeout() {
                            console.log("timeout");
                   }, 1000);

这个例子里,nextTick2和timeout函数无论等待多久都没机会运行,因为事件循环被nextTick函数里的无限循环堵塞了,即使timeout函数被计划在1秒钟后执行它也不会运行。

         当使用setTimeout时,回调函数会被添加到执行计划队列,而在这个例子里它们甚至不会被添加到队列。这虽然是个极端例子,但是你可以看到,运行一个处理器敏感的任务时可能会堵塞或者拖慢事件循环。

退出事件循环

使用process.nextTick,可以把一个非关键性的任务推迟到事件循环的下一轮(tick)再执行,这样可以释放事件循环,让它可以继续执行其它挂起的事件。

看下面例子,如果你打算删除一个临时文件,但是又不想让data事件的回调函数等待这个IO操作,你可以这样延迟它:

                   stream.on("data", function(data) {
                            stream.end("my response");
                            process.nextTick(function() {
                                     fs.unlink("/path/to/file");
                            });
                   });

使用setTimeout替代setInterval来确保函数执行的串行性

假设,你打算设计一个叫my_async_function的函数,它可以做某些I/O操作(比如解析日志文件)的函数,并打算让它周期性执行,你可以用setInterval这样实现它:

                   var interval = 1000;
                   setInterval(function() {
                            my_async_function(function() {
                                     console.log('my_async_function finished!');
                            });
                   },interval);//译者注:前面“,interval”是我添加的,作者应该是笔误遗漏了

你必须能确保这些函数不会被同时执行,但是如果使用setinterval你无法保证这一点,假如my_async_function函数运行的时间比interval变量多了一毫秒,它们就会被同时执行,而不是按次序串行执行。

译者注:(下面粗体部分为译者添加,非原书内容)

为了方便理解这部分内容,可以修改下作者的代码,让它可以实际运行:

                   var interval = 1000;
                   setInterval(function(){
                            (function my_async_function(){
                                      setTimeout(function(){
                                              console.log("1");
                                      },5000);
                           })();
                   },interval);

 运行下这段代码看看,你会发现,等待5秒钟后,“hello ”被每隔1秒输出一次。而我们期望是,当前my_async_function执行完毕(耗费5秒)后,等待1秒再执行下一个my_async_function,每次输出之间应该间隔6秒才对。造成这种结果,是因为my_async_function不是串行执行的,而是多个在同时运行。

 因此,你需要一种办法来强制使一个my_async_function执行结束到下个my_async_function开始执行之间的间隔时间正好是interval变量指定的时间。你可以这样做:
 

                    var interval = 1000; // 1 秒
                   (function schedule() {      //第3行
                            setTimeout(function do_it() {
                                     my_async_function(function() {      //第5行
                                               console.log('async is done!');
                                               schedule();
                                     });
                            }, interval);
                   }());        //第10行

 

前面代码里,声明了一个叫schedule的函数(第3行),并且在声明后立刻调用它(第10行),schedule函数会在1秒(由interval指定)后运行do_it函数。1秒钟过后,第5行的my_async_function函数会被调用,当它执行完毕后,会调用它自己的那个匿名回调函数(第6行),而这个匿名回调函数又会再次重置do_it的执行计划,让它1秒钟后重新执行,这样代码就开始串行地不断循环执行了。

小结

可以用setTimeout()函数预先设定函数的执行计划,并用clearTimeout()函数取消它。还可以用setInterval()周期性的重复执行某个函数,相应的,可以使用clearInterval()取消这个重复执行计划。

如果因为使用了一个处理器敏感的操作而堵塞了事件循环,那些原计划应该被执行的函数将会被延迟,甚至永远无法执行。所以不要在事件循环内使用CPU敏感的操作。还有,你可以使用process.nextTick()把函数的执行延迟到事件循环的下一轮。

I/O和setInterval()一起使用时,你无法保证在任何时间点只有一个挂起的调用,但是,你可以使用递归函数和setTimeout()函数来回避这个棘手的问题。

Javascript 相关文章推荐
javascript事件模型代码
Jul 01 Javascript
通过length属性判断jquery对象是否存在
Oct 18 Javascript
jQuery列表拖动排列具体实现
Nov 04 Javascript
基于jQuery Bar Indicator 插件实现进度条展示效果
Sep 30 Javascript
AngularJS中的$watch(),$digest()和$apply()区分
Apr 04 Javascript
angularJS模态框$modal实例代码
May 27 Javascript
Vue文件配置全局变量的实例
Sep 06 Javascript
js中对象和面向对象与Json介绍
Jan 21 Javascript
js继承的这6种方式!(上)
Apr 23 Javascript
微信小程序获取用户信息的两种方法wx.getUserInfo与open-data实例分析
May 03 Javascript
详解微信小程序胶囊按钮返回|首页自定义导航栏功能
Jun 14 Javascript
vue 动态设置img的src地址无效,npm run build 后找不到文件的解决
Jul 26 Javascript
javascript中实现兼容JAVA的hashCode算法代码分享
Aug 11 #Javascript
javascript实现锁定网页、密码解锁效果(类似系统屏幕保护效果)
Aug 15 #Javascript
javascript使用window.open提示“已经计划系统关机”的原因
Aug 15 #Javascript
Ext4.2的Ext.grid.plugin.RowExpander无法触发事件解决办法
Aug 15 #Javascript
javascript中的__defineGetter__和__defineSetter__介绍
Aug 15 #Javascript
js 判断图片是否加载完以及实现图片的预下载
Aug 14 #Javascript
js创建表单元素并使用submit进行提交
Aug 14 #Javascript
You might like
php 面向对象的一个例子
2011/04/12 PHP
thinkphp 一个页面使用2次分页的实现方法
2013/07/15 PHP
PHP中如何实现常用邮箱的基本判断
2014/01/07 PHP
php实现删除指定目录下相关文件的方法
2014/10/20 PHP
深入理解PHP的远程多会话调试
2017/09/21 PHP
php对象工厂类完整示例
2018/08/09 PHP
通过Javascript创建一个选择文件的对话框代码
2012/06/16 Javascript
onkeydown事件解决按回车键直接提交数据的需求
2013/04/11 Javascript
jquery淡化版banner异步图片文字效果切换图片特效
2014/04/08 Javascript
jQuery获取checkbox选中的值
2016/01/28 Javascript
浅谈json取值(对象和数组)
2016/06/24 Javascript
关于jquery中动态增加select,事件无效的快速解决方法
2016/08/29 Javascript
Easyui的组合框的取值与赋值
2016/10/28 Javascript
js仿iphone秒表功能 计算平均数
2017/01/11 Javascript
d3.js实现立体柱图的方法详解
2017/04/28 Javascript
微信小程序实现移动端滑动分页效果(ajax)
2017/06/13 Javascript
微信小程序tabBar用法实例详解
2017/12/04 Javascript
layui 设置table 行的高度方法
2018/08/17 Javascript
JavaScript使用递归和循环实现阶乘的实例代码
2018/08/28 Javascript
基于JavaScript实现十五拼图代码实例
2020/04/26 Javascript
Python用GET方法上传文件
2015/03/10 Python
Python基于递归实现电话号码映射功能示例
2018/04/13 Python
python3+PyQt5实现支持多线程的页面索引器应用程序
2018/04/20 Python
Django框架基础模板标签与filter使用方法详解
2019/07/23 Python
python模拟哔哩哔哩滑块登入验证的实现
2020/04/24 Python
实例讲解使用CSS实现多边框和透明边框的方法
2015/09/08 HTML / CSS
深入解析HTML5 Canvas控制图形矩阵变换的方法
2016/03/24 HTML / CSS
出门问问全球官方商城:Tichome音箱和TicWatch智能手表
2017/12/02 全球购物
授权委托书范本
2014/04/03 职场文书
英语系本科生求职信
2014/07/15 职场文书
征用土地赔偿协议书
2014/09/26 职场文书
乡镇群众路线整改落实情况汇报
2014/10/28 职场文书
2014年法制宣传日活动方案
2014/11/02 职场文书
2014年林业工作总结
2014/12/05 职场文书
2015年酒店服务员工作总结
2015/05/18 职场文书
赡养老人协议书范本
2015/08/06 职场文书