深刻出不來nodejs源碼-timer模塊(JS篇)

  鴿了很久,最近沉迷遊戲,繼續寫點什麼吧,也不知道有沒有人看。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++部分單獨寫吧。

相關文章
相關標籤/搜索