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


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

      其实这个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.' +
                              '
    Timeout 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++部分单独写吧。

  • 相关阅读:
    element中表单验证实例
    element中时间选择组件,设置default-value无效
    vue中,基于vue-seamless-scroll的无缝滚动实例
    element 表格多选时,修改选中行的背景色
    计算机组成原理11-DMA、数据完整性、分布式计算、大型DMP系统
    计算机组成原理10-总线、输入输出设备、I/O、机械硬盘、SSD硬盘
    计算机组成原理9-CPU组成、缓存、内存
    java基础-字符串
    SQL Server
    BG.Hive
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/9776812.html
Copyright © 2020-2023  润新知