详解Nodejs的timers模块


Posted in NodeJs onDecember 22, 2016

本模块,属于来模拟一些浏览器自带方法的模块,比如setTimeout,clearTimeout等方法,之所以会有该模块,在我看来,也是为了能让前端工程师使用起来,更简单,使用一个单独的模块,来把浏览器上的功能来模拟出来,那么就可以直接减少学习的成本,这样就可以花更少的时间,学习到更多的东西。

timers模块中,使用的C++的方法

timers模块中,调用了C++实现的方法,这些方法,在该模块中,占据了很重要的位置,所以,这里我们先来看下,在C++的方法中,提供了哪些方法。

var Timer = process.binding('timer_wrap').Timer; 
console.log(Timer);

运行之后,在控制台,就会打印出如下的内容,它的格式如下

{ 
 [Function: Timer] 
 // Timer构造函数,可以进行实例化 
 kOnTimeout: 0, 
 // 静态属性,公用,更改会影响其他的调用 
 now: [Function: now] 
 // 静态方法,获取类似时间戳的一个数字 
}

其中,Timer本身是一个构造函数,而这个构造函数中,还包含了一个静态属性和一个静态方法,关于静态属性和方法,基本上,这两个只是拿来使用的,是禁止修改的,并且,其使用方法比较简单,所以这里不多说了。
Timer既然还是一个构造函数,那么久是可以被实例化的,接下来,看下实例化之后的对象:

var Timer = process.binding('timer_wrap').Timer, 
 timer = new Timer(), 
 i = ""; 
 
console.log("obj has attribute:"); 
console.log(timer); 
 
console.log("prototype method and attribute:"); 
for(i in timer){ 
 console.log(i+"="+timer[i]); 
}

把上面的代码,执行的结果如下:

obj has attribute: 
{} 
 
prototype method and attribute: 
close=function close() { [native code] } 
ref=function ref() { [native code] } 
unref=function unref() { [native code] } 
start=function start() { [native code] } 
stop=function stop() { [native code] } 
setRepeat=function setRepeat() { [native code] } 
getRepeat=function getRepeat() { [native code] } 
again=function again() { [native code] }

从上面的结果中可以看出,在Timer实例化之后,在对象本身,是没有属性和方法的,在原型链上,是有一些方法,至于这些方法,有什么用,就需要慢慢去看一下了。

timers模块中的一个基础--构造函数Timeout

之所以这里要把这个构造函数以单小节的形式给出,是因为在我看来,如果想要对整个timers模块中的逻辑有更好的认识,那么该模块的基础一个私有的构造函数的理解,还是很有必要的。

这里,我们首先来看一下源码:

var Timeout = function(after) { 
 // 定义内部属性,过时时间 
 this._idleTimeout = after; 
 
 // 循环链表中的两个属性,可以参考前篇文章linklist私有模块 
 this._idlePrev = this; 
 this._idleNext = this; 
 
 // 记录开始计时时间的属性 
 this._idleStart = null; 
 
 // 当时间到了,执行的回调函数 
 this._onTimeout = null; 
 
 // 该计时器,是否需要repeat,setInterval方法,该属性为true 
 this._repeat = false; 
}; 
 
 
function unrefdHandle() { 
 // unref方法的回调函数,内部this指向Timeout._handle属性 
 // 在该属性上,定义了owner属性,保存Timeout的实例化后的对象 
 this.owner._onTimeout(); 
 if (!this.owner._repeat) 
  this.owner.close(); 
} 
 
Timeout.prototype.unref = function() { 
 // 这个方法,是用来暂停计时器的 
 // 添加一个新的属性_handle用来对接C++提供的API接口 
 if (!this._handle) { 
  // 做一些初始的判断属性,设置初始值等 
  var now = Timer.now(); 
  if (!this._idleStart) this._idleStart = now; 
  var delay = this._idleStart + this._idleTimeout - now; 
  if (delay < 0) delay = 0; 
 
  // 把this指向的计时器对象,清理掉,从计时器链表中清理掉 
  exports.unenroll(this); 
 
  // 介入C++提供的API方法 
  this._handle = new Timer(); 
 
  // 添加一些属性,用来保存一些信息 
  this._handle.owner = this; 
  this._handle[kOnTimeout] = unrefdHandle; 
 
  // 开始计时,在delay后执行改方法的回调 
  this._handle.start(delay, 0); 
  this._handle.domain = this.domain; 
 
  // 调用C++提供的方法,停止计时器的执行 
  this._handle.unref(); 
 } else { 
  // 如果之前有_handle属性,那么则直接停止 
  this._handle.unref(); 
 } 
}; 
 
Timeout.prototype.ref = function() { 
 // 该方法,只有在unref之后,才起作用,恢复计时器的工作 
 // 如果在unref中,生成了_handle属性,那么使用该属性 
 // 调用C++提供的API,ref,恢复计时器的运行 
 if (this._handle) 
  this._handle.ref(); 
}; 
 
Timeout.prototype.close = function() { 
 // 当要关闭计时器对象时,如果定义过接入C++饿API的方法时 
 // 直接使用C++的方法,关闭 
 // 否则,把该方法,清理出去 
 // 不让它再lists链表中,那么当计时器执行到时,也不会执行该计时器的回调函数 
 this._onTimeout = null; 
 if (this._handle) { 
  this._handle[kOnTimeout] = null; 
  // 调用C++中提供的close方法,见前面构造函数Timer的原型链方法中 
  this._handle.close(); 
 } else { 
  exports.unenroll(this); 
 } 
};

上面的源码,就是在timers模块中,内部的一个私有构造函数,在timers公开的一些方法,占据了一个很重要的位子,因为,这个方法,是timers模块,与C++代码链接的重要部分。该部分,是没有示例可以给出的,只有在后面使用timers模块对外公开的API中,来看下对应的使用效果。

这里之所以,要先把这个构造函数放在这里,因为,在我看来,如果能先对这个构造函数有所了解的话,那么接下来看timers模块中的其他方法时,就会变的简单很多。

当然,也有可能是,因为没有看其他的源代码,而导致对于该构造函数的一些方法和属性,很没用感觉的,那么,接下来,就继续看下去吧。

timers模块的源码

timers中的源码,可以分为两部分,在这里,只会看下其中的一部分,还有另外一部分,是和延时执行相反的立即执行的回调函数,这是我们不常用到的,所以这里就不在占用篇幅。

这里,依然使用源码来开始:

'use strict'; 
 
// timer_wrap模块,为底层C++实现的模块 
var Timer = process.binding('timer_wrap').Timer; 
 // Timer在控制台打印出的数据如下: 
 // {[Function: Timer] 是一个构造函数 
 //  kOnTimeout: 0, 
 //  now: [Function: now] 
 // } 
 
// Nodejs模拟的双向链表的操作模块,请查看前一篇关于linklist的文章 
var L = require('_linklist'); 
 
// 断言的管理模块中的ok方法 
var assert = require('assert').ok; 
 
var kOnTimeout = Timer.kOnTimeout | 0; 
 
// Timeout values > TIMEOUT_MAX are set to 1. 
var TIMEOUT_MAX = 2147483647; // 2^31-1 
 
// 把timer添加到debug的模块中,并生成一个函数,命名为debug 
// 在之后,直接调用,该函数,即可把官员timer的错误信息,打印到控制台 
var util = require('util'); 
var debug = util.debuglog('timer'); 
// 注,debuglog方法,应该是最近的版本中,新添加的,因为在一年前,刚接触nodejs时, 
// util模块中,还没有该方法 
 
 
// Object containing all lists, timers 
// key = time in milliseconds 
// value = list 
var lists = {}; 
 
// the main function - creates lists on demand and the watchers associated 
// with them. 
// 把item存入到一个链表中去,并且把msecs对应的链表,存入到lists对象中去 
// lists的格式是这样的: 
 // { 
 // "1000": 这里是一个循环链表,该链表内,包含了所有msecs=1000的list对象 
 // "2000":{} 
 // } 
 
function insert(item, msecs) { 
 // 给item定义两个私有属性 
 // 一个记录当前时间 
 item._idleStart = Timer.now(); 
 // 一个记录毫秒时间,类似于过期时间 
 item._idleTimeout = msecs; 
 
 // 如果定义的毫秒,是负值,则直接返回,不做后面的处理 
 if (msecs < 0) return; 
 
 var list; 
 
 // 如果该过期时间,已经缓存在了lists对象中,则直接找到缓存的数据 
 if (lists[msecs]) { 
  list = lists[msecs]; 
 } else { 
  // 否则,执行新建一个list数据 
  // 并把item和msecs的数据初始化到新创建的对象中去 
  list = new Timer(); 
  // 下面这些,就是Timer实例化之后,包含的方法 
  // close 
  // ref 
  // unref 
  // start 
  // stop 
  // setRepeat 
  // getRepeat 
  // again 
 
  // 实例化之后,调用start方法 
  list.start(msecs, 0); 
 
  // 把list对象,改为一个循环链表 
  L.init(list); 
 
  // 把该list添加到lists对象中缓存 
  // 并设置一些属性,这些属性,在其他方法中被用到 
  lists[msecs] = list; 
  list.msecs = msecs; 
  list[kOnTimeout] = listOnTimeout; 
 } 
 
 // 把item插入到list的下一个节点去 
 L.append(list, item); 
 assert(!L.isEmpty(list)); // list is not empty 
} 
 
// 每一个list的kOnTimeout的属性值,应该是一个回调函数 
// 所以,其内部指向的是list本事 
function listOnTimeout() { 
 var msecs = this.msecs; 
 var list = this; 
 
 debug('timeout callback %d', msecs); 
 
 // 类似一个时间戳,但是又和Date.now()的毫秒级时间戳不同,不知道是如何判断这个的 
 var now = Timer.now(); 
 debug('now: %d', now); 
 
 var diff, first, threw; 
 
 // 当时间到了之后,把对应该时间的链表中的所有元素执行 
 // 如果出现异味,则等一会再次执行,请看源码中的具体注释 
 while (first = L.peek(list)) { 
  // If the previous iteration caused a timer to be added, 
  // update the value of "now" so that timing computations are 
  // done correctly. See test/simple/test-timers-blocking-callback.js 
  // for more information. 
  // 本处的while是,把list的所有前置列表,都处理一遍,直到list所处的链表中,只有list时结束 
  if (now < first._idleStart) { 
   // 当first元素,当执行insert时,会操作_idleStart的属性值 
   // 如果Timer.now的值,是一直增加的,那么这里为神马会执行? 
   // 那么又为什么要有这个判断?只是打了一个log,难道只是为了做个通知? 
   now = Timer.now(); 
   debug('now: %d', now); 
  } 
 
  // 求这个差值?并且与list的msecs值进行判断 
  diff = now - first._idleStart; 
  if (diff < msecs) { 
   // 执行到这里,那边把list继续延时一段时间,因为当前的一个item没有被执行 
   // 所以重新计时,再执行一次 
   list.start(msecs - diff, 0); 
   debug('%d list wait because diff is %d', msecs, diff); 
 
   // 并且直接return,结束本回调函数,等待msecs-diff时间之后,再次执行 
   return; 
  } else { 
   // 把first从它所在的链表中移除 
   L.remove(first); 
 
   // 我觉得,这里是在判断,是否移除成功 
   assert(first !== L.peek(list)); 
 
   // 如果当前的first没有回调函数,那么不需要再向下执行,继续while循环 
   if (!first._onTimeout) continue; 
 
   // 接下来,就是执行回调的处理了,处理的逻辑还行,看起来不算复杂 
   // 只是,有些判断,我现在无法理解到,为什么要这么判断 
 
   // v0.4 compatibility: if the timer callback throws and the 
   // domain or uncaughtException handler ignore the exception, 
   // other timers that expire on this tick should still run. 
   // 
   // https://github.com/joyent/node/issues/2631 
   var domain = first.domain; 
   if (domain && domain._disposed) 
    continue; 
 
   try { 
    if (domain) 
     domain.enter(); 
    threw = true; 
    first._onTimeout(); 
    if (domain) 
     domain.exit(); 
    threw = false; 
   } finally { 
    if (threw) { 
     // We need to continue processing after domain error handling 
     // is complete, but not by using whatever domain was left over 
     // when the timeout threw its exception. 
     var oldDomain = process.domain; 
     process.domain = null; 
     process.nextTick(function() { 
      list[kOnTimeout](); 
     }); 
     process.domain = oldDomain; 
    } 
   } 
  } 
 } 
 
 debug('%d list empty', msecs); 
 assert(L.isEmpty(list)); 
 list.close(); 
 delete lists[msecs]; 
} 
 
 
var unenroll = exports.unenroll = function(item) { 
 L.remove(item); 
 
 // _idleTimeout中保存着msecs的值, 
 // 所有可以根据该属性,直接找到该对象在lists中的缓存数据 
 // 不过,item的msecs中,也保存了list本身的msecs的 
 
 var list = lists[item._idleTimeout]; 
 // if empty then stop the watcher 
 debug('unenroll'); 
 if (list && L.isEmpty(list)) { 
  debug('unenroll: list empty'); 
  // list调用C++的接口 
  list.close(); 
  delete lists[item._idleTimeout]; 
 } 
 // if active is called later, then we want to make sure not to insert again 
 item._idleTimeout = -1; 
 // 本方法,其实就是在清理一些默认的数据了 
 // 属于,当一个方法执行完之后,把其对应的数据,都直接清理掉 
 
}; 
 
 
// Does not start the time, just sets up the members needed. 
exports.enroll = function(item, msecs) { 
 // 给item重新设置一些属性 
 // msecs的值,需要时number类型,并且有效的正整数和零 
 if (!util.isNumber(msecs)) { 
  throw new TypeError('msecs must be a number'); 
 } 
 
 if (msecs < 0 || !isFinite(msecs)) { 
  throw new RangeError('msecs must be a non-negative finite number'); 
 } 
 
 // if this item was already in a list somewhere 
 // then we should unenroll it from that 
 // 保证,item不会存在于两个链表中, 
 // 比如,我最初把item设置为1000之后执行,那么item在lists[1000]所在的链表中 
 // 接下来,我又把item设置为2000之后执行,那么就要先吧item从原来的lists[1000]的链表中删除 
 // 然后,添加到lists[2000]所指向的链表去 
 if (item._idleNext) unenroll(item); 
 
 // Ensure that msecs fits into signed int32 
 // 保证是在最大值之内的,否则,设置为一个系统设置的最大值 
 if (msecs > TIMEOUT_MAX) { 
  msecs = TIMEOUT_MAX; 
 } 
 
 // 设置信息,并初始化item本身的链表 
 item._idleTimeout = msecs; 
 L.init(item); 
}; 
 
 
// call this whenever the item is active (not idle) 
// it will reset its timeout. 
exports.active = function(item) { 
 // 把item插入到缓存的lists对象中, 
 // 或者把已经存在的于对象中的item,进行一次数据更新 
 var msecs = item._idleTimeout; 
 if (msecs >= 0) { 
  // 看上面的函数,enroll可以知道,msecs是必须大于等于0的 
  var list = lists[msecs]; 
  // 如果list存在于lists中,找到对应的链表 
 
  if (!list || L.isEmpty(list)) { 
   // 如果list为空,或者list为空链接,则执行insert方法,创建一个新的链表 
   // 并且把该链表,保存到lists[msecs]中去 
   insert(item, msecs); 
  } else { 
   // 如果有,那么更新item属性的当前时间,把item插入到list链表中去 
   item._idleStart = Timer.now(); 
   L.append(list, item); 
  } 
 } 
}; 
 
 
/* 
* DOM-style timers 
*/ 
 
 
exports.setTimeout = function(callback, after) { 
 // setTimeout的实现源代码 
 // 前两个参数必须是固定的 
 var timer; 
 
 // after转化为数字,或者NaN 
 after *= 1; // coalesce to number or NaN 
 
 // 如果不在合法范围之内,则把after设置为1 
 if (!(after >= 1 && after <= TIMEOUT_MAX)) { 
  after = 1; // schedule on next tick, follows browser behaviour 
 } 
 
 // 根据Timerout构造函数,生成一个实例,该构造函数完成的功能 
 // 只是创建一个对象,设置了一些属性和一些方法 
 timer = new Timeout(after); 
 // 实例化后的timer包含以下内部属性 
 // _idleTimeout = after; 
 // _idlePrev = this; 
 // _idleNext = this; 
 // _idleStart = null; 
 // _onTimeout = null; 
 // _repeat = false; 
 // 以及一下几个原型链方法 
 // unref 
 // ref 
 // close 
 
 // 如果传入的参数,小于等于2个,说明没有多余的默认参数传入 
 if (arguments.length <= 2) { 
  timer._onTimeout = callback; 
 } else { 
 
  // 如果有多余的默认参数传入,那么就要把多余的参数缓存一下 
  // 使用闭包,重新设置一个回调函数 
  var args = Array.prototype.slice.call(arguments, 2); 
  timer._onTimeout = function() { 
   callback.apply(timer, args); 
  } 
 } 
 
 // 设置timer的domain属性为process的domain属性 
 // 该属性,暂时还不知道为什么存在 
 if (process.domain) timer.domain = process.domain; 
 
 // 把timer设置为启动,并在active中,插入到等待执行的列表中去 
 exports.active(timer); 
 
 // 返回Timeout的实例对象,所以,可以想象setTimeout的返回值,到底有哪些属性和方法了吧 
 return timer; 
}; 
 
 
exports.clearTimeout = function(timer) { 
 // 只有timer存在 
 // 回调存在,回调时间存在的情况下,才需要把该方法清理掉 
 // 至于为什么要判断这些条件,请参考listOnTimeout方法内部的注释及逻辑 
 if (timer && (timer[kOnTimeout] || timer._onTimeout)) { 
  timer[kOnTimeout] = timer._onTimeout = null; 
  // 清除回调,时间等属性,然后把timer自lists链表中,去除掉 
  // 这样减少在每次调用时,对lists中对象的无意义的循环 
  if (timer instanceof Timeout) { 
   timer.close(); // for after === 0 
  } else { 
   exports.unenroll(timer); 
  } 
 } 
}; 
 
 
exports.setInterval = function(callback, repeat) { 
 // 前期的处理,和setTimeout方法相同,唯一不同的是,回调 
 // 在本方法中,回调之后,再添加另外一个计时器 
 // 在我看来,就像是每次去调用setTimeout方法一样 
 
 repeat *= 1; // coalesce to number or NaN 
 
 if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) { 
  repeat = 1; // schedule on next tick, follows browser behaviour 
 } 
 
 var timer = new Timeout(repeat); 
 var args = Array.prototype.slice.call(arguments, 2); 
 timer._onTimeout = wrapper; 
 timer._repeat = true; 
 
 if (process.domain) timer.domain = process.domain; 
 exports.active(timer); 
 
 return timer; 
 
 function wrapper() { 
  callback.apply(this, args); 
  // If callback called clearInterval(). 
  if (timer._repeat === false) return; 
  // If timer is unref'd (or was - it's permanently removed from the list.) 
 
  // 下面的处理,是因为在net模块中,和在本模块中,重新启用一个计时器的方法有区别 
  if (this._handle) { 
   // 该分支处理,应该是为了net模块中做的处理 
   // 在本模块中,暂时是没有提及到该属性的 
   this._handle.start(repeat, 0); 
  } else { 
   // 当前的模块中的回调函数 
   timer._idleTimeout = repeat; 
   exports.active(timer); 
  } 
 } 
}; 
 
exports.clearInterval = function(timer) { 
 // 基本上,就是只有timer和repeat属性存在的情况下 
 // 才表示timer对象,是出于Interval方法中, 
 // 这个时候,才去清理掉repeat属性,然后clearTimeout的方法 
 // 清理掉该计时器 
 if (timer && timer._repeat) { 
  timer._repeat = false; 
  clearTimeout(timer); 
 } 
};

timers中的源码,就是这样了,篇幅有限,本篇到这里就结束了,接下来的一篇关于timers模块的文章,将就本篇的源码,结合一些示例,进行一些说明。

总结

像这样的一些模块,感觉突然不知道怎么写了,如果整篇的去放这个源码,感觉这样的文章,完全没有意义的,这样的话,还是应该分开写的吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

NodeJs 相关文章推荐
NodeJs中的非阻塞方法介绍
Jun 05 NodeJs
nodejs6下使用koa2框架实例
May 18 NodeJs
nodejs集成sqlite使用示例
Jun 05 NodeJs
NodeJS链接MySql数据库的操作方法
Jun 27 NodeJs
nodejs构建本地web测试服务器 如何解决访问静态资源问题
Jul 14 NodeJs
nodejs前端自动化构建环境的搭建
Jul 26 NodeJs
nodejs socket服务端和客户端简单通信功能
Sep 14 NodeJs
NodeJS父进程与子进程资源共享原理与实现方法
Mar 16 NodeJs
Linux Centos7.2下安装nodejs&amp;npm配置全局路径的教程
May 15 NodeJs
nodejs微信开发之自动回复的实现
Mar 17 NodeJs
NodeJs 实现简单WebSocket即时通讯的示例代码
Aug 05 NodeJs
浅谈vue websocket nodeJS 进行实时通信踩到的坑
Sep 22 NodeJs
详解nodejs 文本操作模块-fs模块(四)
Dec 22 #NodeJs
详解nodejs 文本操作模块-fs模块(三)
Dec 22 #NodeJs
详解nodejs 文本操作模块-fs模块(一)
Dec 22 #NodeJs
详解nodejs 文本操作模块-fs模块(二)
Dec 22 #NodeJs
学习 NodeJS 第八天:Socket 通讯实例
Dec 21 #NodeJs
详解Nodejs基于mongoose模块的增删改查的操作
Dec 21 #NodeJs
nodejs redis 发布订阅机制封装实现方法及实例代码
Dec 15 #NodeJs
You might like
PHP setcookie() cannot modify header information 的解决方法
2009/01/09 PHP
PHP四舍五入精确小数位及取整
2014/01/14 PHP
推荐一本PHP程序猿都应该拜读的书
2014/12/31 PHP
PHP多文件上传实例
2015/07/09 PHP
laravel如何开启跨域功能示例详解
2017/08/31 PHP
laravel框架之数据库查出来的对象实现转化为数组
2019/10/23 PHP
js禁止小键盘输入数字功能代码
2011/08/01 Javascript
jquery实现弹出层遮罩效果的简单实例
2014/03/03 Javascript
jQuery+ajax实现鼠标单击修改内容的方法
2014/06/27 Javascript
jquery 重写 ajax提交并判断权限后 使用load方法报错解决方法
2016/01/19 Javascript
JavaScript新增样式规则(推荐)
2016/07/19 Javascript
JS实现微信弹出搜索框 多条件查询功能
2016/12/13 Javascript
Three.js基础部分学习
2017/01/08 Javascript
Node.js常用工具之util模块
2017/03/09 Javascript
vue2.0实现移动端的输入框实时检索更新列表功能
2018/05/08 Javascript
vue.js计算属性computed用法实例分析
2018/07/06 Javascript
在vue中安装使用vux的教程详解
2018/09/16 Javascript
微信小程序前端自定义分享的实现方法
2019/06/13 Javascript
Angular8路由守卫原理和使用方法
2019/08/29 Javascript
linux系统使用python获取内存使用信息脚本分享
2014/01/15 Python
使用Python获取CPU、内存和硬盘等windowns系统信息的2个例子
2014/04/15 Python
Python开发WebService系列教程之REST,web.py,eurasia,Django
2014/06/30 Python
python抓取百度首页的方法
2015/05/19 Python
在Python的Django框架中simple-todo工具的简单使用
2015/05/30 Python
Python实现线程状态监测简单示例
2018/03/28 Python
python Web开发你要理解的WSGI &amp; uwsgi详解
2018/08/01 Python
Python实现微信自动好友验证,自动回复,发送群聊链接方法
2019/02/21 Python
python解析yaml文件过程详解
2019/08/30 Python
tensorflow常用函数API介绍
2020/04/19 Python
施华洛世奇水晶荷兰官方网站:SWAROVSKI荷兰
2017/05/12 全球购物
什么是符号链接,什么是硬链接?符号链接与硬链接的区别是什么?
2014/01/19 面试题
法学专业个人求职信
2013/09/26 职场文书
班组长岗位职责范本
2014/01/05 职场文书
环保倡议书50字
2014/05/15 职场文书
四风个人对照检查材料思想汇报(办公室通用版)
2014/10/07 职场文书
python requests模块的使用示例
2021/04/07 Python