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 相关文章推荐
读jQuery之十四 (触发事件核心方法)
Aug 23 Javascript
Javascript仿PHP $_GET获取URL中的参数
May 12 Javascript
jQuery 插件开发指南
Nov 14 Javascript
javascript检测flash插件是否被禁用的方法
Jan 14 Javascript
TypeScript Type Innference(类型判断)
Mar 10 Javascript
js判断是否为空和typeof的用法(详解)
Oct 07 Javascript
Bootstrap导航条可点击和鼠标悬停显示下拉菜单
Nov 25 Javascript
Angular实现购物车计算示例代码
Feb 21 Javascript
js前端实现图片懒加载(lazyload)的两种方式
Apr 24 Javascript
微信小程序 获取二维码实例详解
Jun 23 Javascript
各种选择框jQuery的选中方法(实例讲解)
Jun 27 jQuery
vue实现直播间点赞飘心效果的示例代码
Sep 20 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对象类型判断
2008/08/27 PHP
php 方便水印和缩略图的图形类
2009/05/21 PHP
C# Assembly类访问程序集信息
2009/06/13 PHP
php 上传功能实例代码
2010/04/13 PHP
php选择排序法实现数组排序实例分析
2015/02/16 PHP
JS无限树状列表实现代码
2011/01/11 Javascript
基于jquery的滚动鼠标放大缩小图片效果
2011/10/27 Javascript
Javascript实现多彩雪花从天降散落效果的方法
2015/02/02 Javascript
JQuery中属性过滤选择器用法实例分析
2015/05/18 Javascript
jQuery实现发送验证码并60秒倒计时功能
2016/11/25 Javascript
jQuery表格(Table)基本操作实例分析
2017/03/10 Javascript
微信小程序 登录的简单实现
2017/04/19 Javascript
React中的refs的使用教程
2018/02/13 Javascript
VUE 单页面使用 echart 窗口变化时的用法
2020/07/30 Javascript
在unittest中使用 logging 模块记录测试数据的方法
2018/11/30 Python
Python 运行 shell 获取输出结果的实例
2019/01/07 Python
python pytest进阶之conftest.py详解
2019/06/27 Python
Python虚拟环境venv用法详解
2020/05/25 Python
Python使用20行代码实现微信聊天机器人
2020/06/05 Python
Python如何使用input函数获取输入
2020/08/06 Python
Python使用Pygame绘制时钟
2020/11/29 Python
如何用python实现一个HTTP连接池
2021/01/14 Python
html5 offlline 缓存使用示例
2013/06/24 HTML / CSS
Shopee新加坡:东南亚与台湾电商平台
2019/01/25 全球购物
ruby如何进行集成操作?Ruby能进行多重继承吗?
2013/10/16 面试题
大客户销售经理职责
2013/12/04 职场文书
九年级历史教学反思
2014/01/27 职场文书
士力架广告词
2014/03/20 职场文书
上海世博会志愿者口号
2014/06/17 职场文书
个人授权委托书样本
2014/09/13 职场文书
2014年幼儿园安全工作总结
2014/11/10 职场文书
2014年客服工作总结范文
2014/11/13 职场文书
学校2014年度工作总结
2014/12/06 职场文书
先进工作者事迹材料
2014/12/23 职场文书
2016三八妇女节校园广播稿
2015/12/17 职场文书
JavaScript中10个Reduce常用场景技巧
2022/06/21 Javascript