鸽了很久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看。html
其实这个node的源码也不知道该怎么写了,不少模块涉及的东西比较深,JS和C++两头看,中间被工做耽搁回来就一脸懵逼了,因此仍是挑一些简单的吧!node
这一篇选的是定时器模块,简单讲就是初学者都很是熟悉的setTimeout与setInterval啦,源码的JS内容在目录lib/timers.js中。api
node的定时器模块是本身单独实现的,与Chrome的window.setTimeout可能不太同样,可是思想应该都是相通的,学一学总没错。数组
链表数据结构
定时器模块实现中有一个关键数据结构:链表。用JS实现的链表,大致上跟其余语言的链表的原理仍是同样,每个节点内容可分为前指针、后指针、数据。app
源码里的链表构造函数有两种,一个是List的容器,一个是容器里的item。异步
这里看看List:async
function TimersList(msecs, unrefed) { // 前指针 this._idleNext = this; // 后指针 this._idlePrev = this; // 数据 this._unrefed = unrefed; this.msecs = msecs; // ...更多 }
这是一个很典型的链表例子,包含2个指针(属性)以及数据块。item的构造函数大同小异,也是包含了两个指针,只是数据内容有些不一样。函数
关于链表的操做,放在了一个单独的JS文件中,目录在lib/internal/linkedlist.js,实现跟C++、Java内置的有些许不同。测试
看一下增删就差很少了,首先看删:
function remove(item) { // 处理先后节点的指针指向 if (item._idleNext) { item._idleNext._idlePrev = item._idlePrev; } if (item._idlePrev) { item._idlePrev._idleNext = item._idleNext; } // 重置节点自身指针指向 item._idleNext = null; item._idlePrev = null; }
关于数据结构的代码,都是虽然看起来少,可是理解起来都有点恶心,能画出图就差很少了,因此这里给一个简单的示意图。
应该能看懂吧……反正中间那个假设就是item,首先让先后两个对接上,而后把自身的指针置null。
接下来是增。
function append(list, item) { // 先保证传入节点是空白节点 if (item._idleNext || item._idlePrev) { remove(item); } // 处理新节点的头尾连接 item._idleNext = list._idleNext; item._idlePrev = list; // 处理list的前指针指向 list._idleNext._idlePrev = item; list._idleNext = item; }
这里须要注意,初始化的时候就有一个List节点,该节点只做为链表头,与其他item不同,一开始先后指针均指向本身。
以上是append节点的三步示例图。
以前说过JS实现的链表与C++、Java有些许不同,就在这里,每一次添加新节点时:
C++/Java:node-node => node-node-new
JS(node):list-node-node => list-new-node-node
总的来讲,JS用了一个list来做为链表头,每一次添加节点都是往前面塞,总体来说是一个双向循环链表。
而在C++/Java中则是能够选择,API丰富多彩,链表类型也分为单向、单向循环、双向等。
setTimeout
链表有啥用,后面就知道了。
首先从setTimeout这个典型的API入手,node的调用方式跟window.setTimeout一致,因此就不介绍了,直接上代码:
/** * * @param {Function} callback 延迟触发的函数 * @param {Number} after 延迟时间 * @param {*} arg1 额外参数1 * @param {*} arg2 额外参数2 * @param {*} arg3 额外参数3 */ function setTimeout(callback, after, arg1, arg2, arg3) { // 只有第一个函数参数是必须的 if (typeof callback !== 'function') { throw new ERR_INVALID_CALLBACK(); } var i, args; /** * 参数修正 * 简单来讲 就是将第三个之后的参数包装成数组 */ switch (arguments.length) { case 1: case 2: break; case 3: args = [arg1]; break; case 4: args = [arg1, arg2]; break; default: args = [arg1, arg2, arg3]; for (i = 5; i < arguments.length; i++) { args[i - 2] = arguments[i]; } break; } // 生成一个Timeout对象 const timeout = new Timeout(callback, after, args, false, false); active(timeout); // 返回该对象 return timeout; }
能够看到,调用方式基本一致,可是有一点很不同,该方法返回的不是一个表明定时器ID的数字,而是直接返回生成的Timeout对象。
稍微测试一下:
虽说返回的是对象,可是clearTimeout须要的参数也正是一个timeout对象,整体来讲也没啥须要注意的。
Timeout
接下来看看这个对象的内容,源码来源于lib/internal/timers.js。
/** * * @param {Function} callback 回调函数 * @param {Number} after 延迟时间 * @param {Array} args 参数数组 * @param {Boolean} isRepeat 是否重复执行(setInterval/setTimeout) * @param {Boolean} isUnrefed 不知道是啥玩意 */ function Timeout(callback, after, args, isRepeat, isUnrefed) { /** * 对延迟时间参数进行数字类型转换 * 数字类型字符串 会变成数字 * 非数字非数字字符串 会变成NaN */ after *= 1; if (!(after >= 1 && after <= TIMEOUT_MAX)) { // 最大为2147483647 官网有写 if (after > TIMEOUT_MAX) { process.emitWarning(`${after} does not fit into` + ' a 32-bit signed integer.' + '\nTimeout duration was set to 1.', 'TimeoutOverflowWarning'); } // 小于一、大于最大限制、非法参数均会被重置为1 after = 1; } // 调用标记 this._called = false; // 延迟时间 this._idleTimeout = after; // 先后指针 this._idlePrev = this; this._idleNext = this; this._idleStart = null; // V8层面的优化我也不太懂 留下英文注释本身研究吧 // this must be set to null first to avoid function tracking // on the hidden class, revisit in V8 versions after 6.2 this._onTimeout = null; // 回调函数 this._onTimeout = callback; // 参数 this._timerArgs = args; // setInterval的参数 this._repeat = isRepeat ? after : null; // 摧毁标记 this._destroyed = false; this[unrefedSymbol] = isUnrefed; // 暂时不晓得干啥的 initAsyncResource(this, 'Timeout'); }
以前讲过,整个方法,只有第一个参数是必须的,若是不传延迟时间,默认设置为1。
这里有意思的是,若是传一个字符串的数字,也是合法的,会被转换成数字。而其他非法值会被转换为NaN,且NaN与任何数字比较都返回false,因此始终会重置为1这个合法值。
后面的属性基本上就能够分为两个指针和数据块了,最后的initAsyncResource目前还没搞懂,其他模块也见过这个东西,先留个坑。
这里的initAsyncResource是一个实验中的API,做用是为异步资源添加钩子函数,详情可见:http://nodejs.cn/api/async_hooks.html
active/insert
生成了Timeout对象,第三步就会利用前面的链表进行处理,这里才是重头戏。
const refedLists = Object.create(null); const unrefedLists = Object.create(null); const active = exports.active = function(item) { insert(item, false); }; /** * * @param {Timeout} item 定时器对象 * @param {Boolean} unrefed 区份内部/外部调用 * @param {Boolean} start 不晓得干啥的 */ function insert(item, unrefed, start) { // 取出延迟时间 const msecs = item._idleTimeout; if (msecs < 0 || msecs === undefined) return; if (typeof start === 'number') { item._idleStart = start; } else { item._idleStart = TimerWrap.now(); } // 内部使用定时器使用不一样对象 const lists = unrefed === true ? unrefedLists : refedLists; // 延迟时间做为键来生成一个链表类型值 var list = lists[msecs]; if (list === undefined) { debug('no %d list was found in insert, creating a new one', msecs); lists[msecs] = list = new TimersList(msecs, unrefed); } // 留个坑 暂时不懂这个 if (!item[async_id_symbol] || item._destroyed) { item._destroyed = false; initAsyncResource(item, 'Timeout'); } // 把当前timeout对象添加到对应的链表上 L.append(list, item); assert(!L.isEmpty(list)); }
从这能够看出node内部处理定时器回调函数的方式。
首先有两个空对象,分别保存内部、外部的定时器对象。对象的键是延迟时间,值则是一个链表头,即之前介绍的list。每一次生成一个timeout对象时,会连接到list后面,经过这个list能够引用到全部该延迟时间的对象。
画个图示意一下:
那么问题来了,node是在哪里开始触发定时器的?实际上,在生成对应list链表头的时候就已经开始触发了。
完整的list构造函数源码以下:
function TimersList(msecs, unrefed) { this._idleNext = this; this._idlePrev = this; this._unrefed = unrefed; this.msecs = msecs; // 来源于C++内置模块 const timer = this._timer = new TimerWrap(); timer._list = this; if (unrefed === true) timer.unref(); // 触发 timer.start(msecs); }
最终仍是指向了内置模块,将list自己做为属性添加到timer上,经过C++代码触发定时器。
C++部分单独写吧。