欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

深入出不来nodejs源码-timer模块(JS篇)

程序员文章站 2022-06-24 15:25:21
鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看。 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单的吧! 这一篇选的是定时器模块,简单讲就是初学者都非常熟悉的setTimeout与setInterv ......

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

  其实这个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;
}

  关于数据结构的代码,都是虽然看起来少,但是理解起来都有点恶心,能画出图就差不多了,所以这里给一个简单的示意图。

深入出不来nodejs源码-timer模块(JS篇)

  应该能看懂吧……反正中间那个假设就是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不一样,一开始前后指针均指向自己。

深入出不来nodejs源码-timer模块(JS篇)

  以上是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对象。

  稍微测试一下:

深入出不来nodejs源码-timer模块(JS篇)

  虽然说返回的是对象,但是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目前还没搞懂,其余模块也见过这个东西,先留个坑。

 

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可以引用到所有该延迟时间的对象。

  画个图示意一下:

深入出不来nodejs源码-timer模块(JS篇)

  那么问题来了,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++部分单独写吧。