1. 程式人生 > >深入出不來nodejs原始碼-timer模組(JS篇)

深入出不來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目前還沒搞懂,其餘模組也見過這個東西,先留個坑。

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++部分單獨寫吧。