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