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 相关文章推荐
用js实现键盘方向键翻页功能的代码
Jun 03 Javascript
ppk谈JavaScript style属性
Oct 10 Javascript
javascript 有用的脚本函数
May 07 Javascript
extJs 常用到的增,删,改,查操作代码
Dec 28 Javascript
使用jQuery的easydrag插件实现可拖动的DIV弹出框
Feb 19 Javascript
浅谈JavaScript中面向对象的的深拷贝和浅拷贝
Aug 01 Javascript
jQuery中ScrollTo用法示例
Sep 04 Javascript
微信小程序之数据双向绑定与数据操作
May 12 Javascript
jQuery复合事件结合toggle()方法的用法示例
Jun 10 jQuery
vue.js过滤器+ajax实现事件监听及后台php数据交互实例
May 22 Javascript
ES6入门教程之let、const的使用方法
Apr 13 Javascript
JS事件流与事件处理程序实例分析
Aug 16 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
Joomla下利用configuration.php存储简单数据
2010/05/19 PHP
php打乱数组二维数组多维数组的简单实例
2016/06/17 PHP
TP5(thinkPHP5)框架使用ajax实现与后台数据交互的方法小结
2020/02/10 PHP
JavaScript 组件之旅(三):用 Ant 构建组件
2009/10/28 Javascript
Zero Clipboard js+swf实现的复制功能使用方法
2010/03/07 Javascript
jQuery最佳实践完整篇
2011/08/20 Javascript
JS分页控件 可用于无刷新分页
2013/07/23 Javascript
JS实现侧悬浮浮动实例代码
2013/11/29 Javascript
前台js调用后台方法示例
2013/12/02 Javascript
window.location.href中url中数据量太大时的解决方法
2013/12/23 Javascript
Javascript 遍历页面text控件详解
2014/01/06 Javascript
bootstrap改变按钮加载状态
2014/12/01 Javascript
JavaScript数组去重的3种方法和代码实例
2015/07/01 Javascript
JavaScript中有关一个数组中最大值和最小值及它们的下表的输出的解决办法
2016/07/01 Javascript
详解JS对象封装的常用方式
2016/12/30 Javascript
javascript基本数据类型和转换
2017/03/17 Javascript
js前端导出Excel的方法
2017/11/01 Javascript
微信小程序实现打开内置地图功能【附源码下载】
2017/12/07 Javascript
Python用模块pytz来转换时区
2016/08/19 Python
Python编程二分法实现冒泡算法+快速排序代码示例
2018/01/15 Python
彻彻底底地理解Python中的编码问题
2018/10/15 Python
深入浅析Python 中的sklearn模型选择
2019/10/12 Python
使用Python实现批量ping操作方法
2020/05/06 Python
利用pipenv和pyenv管理多个相互独立的Python虚拟开发环境
2020/11/01 Python
HTML5实现移动端点击翻牌功能
2020/10/23 HTML / CSS
MIS软件工程师的面试题
2016/04/22 面试题
四种会话跟踪技术
2015/05/20 面试题
高中历史教学反思
2014/02/08 职场文书
《观舞记》教学反思
2014/04/16 职场文书
小学网上祭英烈活动总结
2014/07/05 职场文书
一年级班主任工作总结2014
2014/11/08 职场文书
2014年世界艾滋病日宣传活动总结
2014/11/18 职场文书
2014年机关后勤工作总结
2014/12/16 职场文书
治庸问责工作总结
2015/08/11 职场文书
2016计划生育先进个人事迹材料
2016/02/29 职场文书
Go语言使用select{}阻塞main函数介绍
2021/04/25 Golang