深入出不來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.' +
                          '\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++部分單獨寫吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM