鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看。

  其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单的吧!

  

  这一篇选的是定时器模块,简单讲就是初学者都非常熟悉的setTimeout与setInterval啦,源码的JS内容在目录lib/timers.js中。

  node的定时器模块是自己单独实现的,与Chrome的window.setTimeout可能不太一样,但是思想应该都是相通的,学一学总没错。

链表

  定时器模块实现中有一个关键数据结构:链表。用JS实现的链表,大体上跟其他语言的链表的原理还是一样,每一个节点内容可分为前指针、后指针、数据。

  源码里的链表构造函数有两种,一个是List的容器,一个是容器里的item。

  这里看看List:

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、大于最大限制、非法参数均会被重置为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++部分单独写吧。

深入出不来nodejs源码-timer模块(JS篇)的更多相关文章

  1. 深入出不来nodejs源码-timer模块(C++篇)

    终于可以填上坑了. 简单回顾一下之前JS篇内容,每一次setTimeout的调用,会在一个对象中添加一个键值对,键为延迟时间,值为一个链表,将所有该时间对应的事件串起来,图如下: 而每一个延迟键值对的 ...

  2. 深入出不来nodejs源码-events模块

    这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼. 学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定.这个东西贯穿了整个JS ...

  3. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

  4. 深入出不来nodejs源码-编译启动(1)

    整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...

  5. 深入出不来nodejs源码-从fs.stat方法来看node架构

    node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链. 首先上一张整体的图,网上翻到的,自己懒得画: 这里的层次结构十分的清晰, ...

  6. 深入出不来nodejs源码-内置模块引入再探

    我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...

  7. 深入出不来nodejs源码-内置模块引入初探

    重新审视了一下上一篇的内容,配合源码发现有些地方说的不太对,或者不太严谨. 主要是关于内置模块引入的问题,当时我是这样描述的: 需要关注的只要那个RegisterBuiltinModules方法,从名 ...

  8. 深入出不来nodejs源码-流程总览

    花了差不多两周时间过了下primer C++5th,完成了<C++从入门到精通>.(手动滑稽) 这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造 ...

  9. JUC源码分析-线程池篇(三)Timer

    JUC源码分析-线程池篇(三)Timer Timer 是 java.util 包提供的一个定时任务调度器,在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次. 1. Ti ...

随机推荐

  1. Opencv打开摄像头,读不到图像,一般来说先读取第一帧,舍弃,然后就正常了

    舍弃第一帧的程序: cap >> img; cv::waitKey(100);  if (cvWaitKey(5) == 27) break; cap >> img;

  2. EntityFramework Core 学习扫盲

    0. 写在前面 1. 建立运行环境 2. 添加实体和映射数据库 1. 准备工作 2. Data Annotations 3. Fluent Api 3. 包含和排除实体类型 1. Data Annot ...

  3. node.js global object,util and so on

    核心模块主要内容: 全局对象 常用工具 事件机制 文件系统访问 http服务器和客户端 global object: 所有的全局变量(除了global本身外)都是global object 的属性(a ...

  4. Markdown中怎么上传图片

    在网站中使用了Markdown编辑器,但是不能支持图片的直接粘贴

  5. 【文文殿下】NOIp2018游记

    Day-1 本段更新于 2018年11月8日23:26:44 今天还在机房里面,无所事事吧.上午睡了一上午,出去理了一下发,花了20块钱 QAQ. 下午来到机房,复习了一下exgcd的东西. 发现自己 ...

  6. Python 库,资源

    库名称简介 Chardet字符编码探测器,可以自动检测文本.网页.xml的编码. colorama主要用来给文本添加各种颜色,并且非常简单易用. Prettytable主要用于在终端或浏览器端构建格式 ...

  7. USACO December 铂金Maxflow

    USACO 2015 December Contest, Platinum Problem 1. Max Flow Farmer John has installed a new system of ...

  8. 解压cpio.gz

    #gunzip 文件名.cpio.gz #cpio -idmv < 文件名.cpio

  9. .NET Core 常用加密和Hash工具NETCore.Encrypt

    前言 在日常开发过程中,不可避免的涉及到数据加密解密(Hash)操作,所以就有想法开发通用工具,NETCore.Encrypt就诞生了.目前NETCore.Encrypt只支持.NET Core ,工 ...

  10. UICollectionView设置首个cell默认选中(二)

    上篇对于UICollectionView默认选中cell采取的是每个cell分别对应一个标识,也就代表着废除了UICollectionView的重用机制.对于较少的数据情况是可以的,但是对于数据比较大 ...