《深刻淺出Nodejs》筆記——模塊機制(1)

前言

這是我讀《深刻淺出Nodejs》的筆記,真是但願個人機械鍵盤快點到啊,累死我了。javascript

CommonJS規範

主要分爲模塊引用、模塊定義、模塊標識三個部分。java

模塊引用

上下文提供require()方法來引入外部模塊,示例代碼以下:node

//test.js
//引入一個模塊到當前上下文中
var math = require('math');
math.add(1, 2);

模塊定義

上下文提供了exports對象用於導入導出當前模塊的方法或者變量,而且它是惟一的導出出口。模塊中存在一個module對象,它表明模塊自身,exports是module的屬性。一個文件就是一個模塊,將方法做爲屬性掛載在exports上就能夠定義導出的方式:json

//math.js
exports.add = function () {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while(i < l) {
        sum += args[i++];
    }
    return sum;
}

這樣就可像test.js裏那樣在require()以後調用模塊的屬性或者方法了。數組

模塊標識

模塊標識就是傳遞給require()方法的參數,它必須是符合小駝峯命名的字符串,或者以"."、".."開頭的相對路徑或者絕對路徑,能夠沒有文件後綴名".js".瀏覽器

Node的模塊實現

在Node中引入模塊,須要經歷以下三個步驟:緩存

  1. 路徑分析
  2. 文件定位
  3. 編譯執行

在Node中模塊分爲兩類:一是Node提供的模塊,稱爲核心模塊;另外一類是用戶編寫的模塊,稱爲文件模塊。
核心模塊是Node源碼在編譯過程當中編譯進了二進制執行文件。在Node啓動時這些模塊就被加載進內存中,因此核心模塊引入時省去了文件定位和編譯執行兩個步驟,而且在路徑分析中優先判斷,所以核心模塊的加載速度是最快的。工具

文件模塊則是在運行時動態加載,速度比核心模塊慢。性能

優先從緩存加載

和瀏覽器會緩存靜態js文件同樣,Node也會對引入的模塊進行緩存,不一樣的是瀏覽器緩存的是文件,Node緩存的是編譯執行以後的對象。
require()對相同模塊的二次加載一概採用緩存優先的方式,這是第一優先級的,核心模塊緩存檢查先於文件模塊的緩存檢查。ui

路徑分析和文件定位

由於標識符有幾種形式,對於不一樣的標識符,模塊的查找和定位有不一樣程度上的差別。

1.模塊標識符分析

模塊標識符在Node中主要分爲如下幾類:

  • 核心模塊
  • 相對路徑文件模塊
  • 絕對路徑文件模塊
  • 非路徑形式的文件模塊
核心模塊

核心模塊優先級僅次於緩存加載,所以沒法加載一個和核心模塊標識符相同的自定義模塊。

路徑形式的文件模塊

以"."、".."開頭和"/"開始的標識符,這裏都被看成文件模塊來處理。require()方法會將路徑轉爲真實路徑,並以真實路徑做爲索引,並將編譯執行後的結果存放到緩存中。

自定義模塊

自定義模塊是指非核心模塊,也不是路徑形式的標識符。它是一種特殊的文件模塊,多是一個文件或者包的形式。
模塊路徑是Node在定位文件模塊的具體文件時制定的查找策略,具體表現爲一個路徑組成的數組(module.paths)。這個路徑由當前目錄開始往上一直到根目錄,Node會逐個嘗試模塊路徑中的路徑,直到找到目標文件未知,若到達根目錄仍是沒有找到目標文件,則會拋出查找失敗的異常。當前文件的目錄越深,模塊查找耗時越多。

2.文件定位

文件擴展名分析

調用require()方法時若參數沒有文件擴展名,Node會按.js、.json、.node的順尋補足擴展名,依次嘗試。
在嘗試過程當中,須要調用fs模塊阻塞式地判斷文件是否存在。由於Node是單線程的,這是一個會引發性能問題的地方。若是是.node或者.json文件能夠加上擴展名加快一點速度。另外一個訣竅是:同步配合緩存。

目錄分析和包

require()分析文件擴展名後,可能沒有查到對應文件,而是找到了一個目錄,此時Node會將目錄看成一個包來處理。
首先, Node在擋牆目錄下查找package.json,經過JSON.parse()解析出包秒速對象,從中取出main屬性指定的文件名進行定位。若main屬性指定文件名錯誤,或者沒有pachage.json文件,Node會將index看成默認文件名。
若目錄分析沒有定位成功任何文件,則自定義模塊進入下一個模塊路徑。

模塊編譯

每一個模塊文件模塊都是一個對象,它的定義以下:

function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if(parent && parent.children) {
        parent.children.push(this);
    }

    this.filename = null;
    this.loaded = false;
    this.children = [];
}

對於不一樣擴展名,其載入方法也有所不一樣:

  • .js 經過fs模塊同步讀取文件後編譯執行。
  • .node 這是C/C++編寫的擴展文件,經過dlopen()方法加載最後編譯生成的文件
  • .json 同過fs模塊同步讀取文件後,用JSON.pares()解析返回結果
  • 其餘 看成.js

每個編譯成功的模塊都會將其文件路徑做爲索引緩存在Module._cache對象上。

.json文件調用的方法以下:

//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
    try {
        module.exports = JSON.parse(stripBOM(content));
    } catch(err) {
        err.message = filename + ':' + err.message;
        throw err;
    }
}

Module._extensions會被賦值給require()的extensions屬性,因此能夠用:console.log(require.extensions);輸出系統中已有的擴展加載方式。
固然也能夠本身增長一些特殊的加載:require.extensions['.txt'] = function(){//code};

可是從v0.10.6版本開始官方不鼓勵經過這種方式自定義擴展名加載,而是指望先將其餘語言或文件編譯成JavaScript文件後再加載,這樣的好處在於不講煩瑣的編譯加載等過程引入Node的執行過程。

1.JavaScript模塊的編譯

在編譯的過程當中,Node對獲取的javascript文件內容進行了頭尾包裝,將文件內容包裝在一個function中:

(function (exports, require, module, _filename, _dirname) {
    //js文件內容
});

包裝以後的代碼會經過vm原生模塊的runInThisContext()方法執行(具備明確上下文,不污染全局),返回一個具體的function對象,最後傳參執行,執行後返回model.exports.

核心模塊

核心模塊分爲C/C++編寫和JavaScript編寫的兩個部分,其中C/C++文件放在Node項目的src目錄下,JavaScript文件放在lib目錄下。

JavaScript核心模塊的編譯過程

1.轉存爲C/C++代碼

Node採用了V8附帶的js2c.py工具,將全部內置的JavaScript代碼轉換成C++裏的數組,生成node_natives.h頭文件:

namespace node {
    const char node_native[] = { 47, 47, ..};
    const char dgram_native[] = { 47, 47, ..};
    const char console_native = { 47, 47, ..};
    const char buffer_native = { 47, 47, ..};
    const char querystring_native = { 47, 47, ..};
    const char punycode_native = { 47, 47, ..};
    ...
    struct _native {
        const char* name;
        const char* source;
        size_t source_len;
    }

    static const struct _native natives[] = {
      { "node", node_native, sizeof(node_native)-1},
      { "dgram", dgram_native, sizeof(dgram_native)-1},
      ...
    };
}

在這個過程當中,JavaScript代碼以字符串形式存儲在node命名空間中,是不可直接執行的。在啓動Node進程時,js代碼直接加載到內存中。在加載的過程當中,js核心模塊經歷標識符分析後直接定位到內存中。

2.編譯js核心模塊

lib目錄下的模塊文件也在引入過程當中經歷了頭尾包裝的過程,而後才執行和導出了exports對象。與文件模塊的區別在於:獲取源代碼的方式(核心模塊從內存加載)和緩存執行結果的位置。
js核心模塊源文件經過process.binding('natives')取出,編譯成功的模塊緩存到NativeModule._cache上。代碼以下:

function NativeModule() {
    this.filename = id + '.js';
    this.id = id;
    this.exports = {};
    this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
相關文章
相關標籤/搜索