痛覺殘留——Node原理

模塊原理

在開始 Node 原理剖析之前,想講一個十分簡單的題目:module.exports 和 exports 有什么區(qū)別?這道題也算是面試必問題目之一了,答不出來面試基本就涼涼了。這里為尚未了解到的猿同胞們再解釋一遍。

1.exports 是 module.exports 的一個引用,即 exports = module.exports = {},二者所存的地址變量指向同一個對象。
2.暴露其實只會暴露出 module.exports 指向的對象,所以如果 exports 所存的地址指向另一個對象,則無法暴露,這也是為什么不能直接給 exports 賦值(exports = xxx)。

再深問一點:為什么暴露的是 module.exports?可能已經(jīng)有猿同胞了解過。

1.Node 執(zhí)行每個 js 文件時,其實是把內(nèi)容包裹在一個函數(shù)中,然后執(zhí)行這個函數(shù)。
2.這個函數(shù)傳入的參數(shù)共有 exports、require、module、__filename、__dirname,所以我們可以在 js 文件中直接使用這些參數(shù)。而其它模塊導(dǎo)入時會導(dǎo)入 module 的 exports 屬性值,而不是 exports 引用。

那繼續(xù)深挖:Node 是如何給文件包裹函數(shù)的?如何執(zhí)行這個函數(shù)?exports / module.exports 和 require 是如何實現(xiàn)的?本篇就將解答這三個問題。

論據(jù):Node 自帶許多模塊,且目前仍遵循 CommonJS 規(guī)范:

1.在 CommonJS 規(guī)范中,一個文件就是一個模塊。
2.在 CommonJS 規(guī)范中,exports 暴露模塊數(shù)據(jù),require 導(dǎo)入模塊數(shù)據(jù)。
3.Node 模塊中,fs 文件模塊可以讀取文件為二進(jìn)制或字符串。(顯然二進(jìn)制或字符串都無法執(zhí)行)
4.Node 模塊中,vm 模塊具有安全虛擬機(jī)環(huán)境可以將字符串轉(zhuǎn)化為代碼執(zhí)行。

結(jié)論:require 導(dǎo)入模塊數(shù)據(jù) + 文件 = 模塊 => require 讀取文件。因此,在 require 函數(shù)中,本質(zhì)是根據(jù)傳入的路徑參數(shù),用 fs 模塊讀取相應(yīng)的文件為字符串,在字符串前后拼接被轉(zhuǎn)化成字符串的函數(shù),用 vm 虛擬機(jī)執(zhí)行函數(shù)。

在使用 vm 虛擬機(jī)之前,補(bǔ)充一個知識點,runInThisContext 和 runInNewContext 的區(qū)別。

引入 vm 模塊,通過 vm 的 runInThisContext 方法執(zhí)行字符串,該字符串必須可以轉(zhuǎn)化為代碼執(zhí)行。

image
image

可見是可以執(zhí)行的,那么可不可以植入變量,尤其是虛擬機(jī)所處文件的變量。

image
image

報錯 str 未定義,可見虛擬機(jī)外部變量是無法訪問的,但我們知道,node 是存在全局變量的,虛擬機(jī)和虛擬機(jī)所在文件共處一個 node 環(huán)境,那么全局變量能否植入。

image
image

又正常輸出了,那如何將虛擬機(jī)和虛擬機(jī)所在文件所處環(huán)境分開,那就需要使用 runInNewContext。

image
image

再次報錯,可見 runInNewContext 無法訪問 global 中的變量。

回到正題,繼續(xù)看 Node 如何實現(xiàn) require,首先肯定需要定義一個 require 函數(shù),忽略內(nèi)部的 try finally,require 函數(shù)體僅剩一句,self 就代表執(zhí)行 require 的模塊,相當(dāng)于執(zhí)行每個模塊自身的 require 方法。

// 原版
function require(path) {
  try {
    exports.requireDepth += 1;
    return self.require(path);
  } finally {
    exports.requireDepth -= 1;
  }
}
// 忽略try finally
function require(path) {
    return self.require(path);
}

而模塊自身的 require 方法所在文件引入了 assert 斷言庫,斷言庫內(nèi)容我在《遺落之城——Unit Test》中有寫過。這里斷言 path 必須存在且必須是 string 類型,成功后調(diào)用模塊的_load 靜態(tài)方法,而_load 本質(zhì)其實是加載已被加載過的模塊,并不是真正加載模塊的函數(shù)。

Module.prototype.require = function (path) {
  assert(path, "missing path");
  assert(typeof path === "string", "path must be a string");
  return Module._load(path, this, /* isMain */ false);
};

先貼上_load 方法的完整代碼,再一句句分析。

1.Module._resolveFilename 是將 request 相對路徑情況轉(zhuǎn)為絕對路徑,如果 request 是絕對路徑就原樣返回,還有一種情況是 node 原生模塊,也會原樣返回,同時 filename 也會作為該模塊的唯一標(biāo)識。
2.先從 Module 的靜態(tài)屬性_cache(緩存)上查找,如果已有緩存,則直接返回緩存的 exports。
3.通過 NativeModule.nonInternalExists 判斷是否是原生模塊,如果是則調(diào)用原生模塊 NativeModule 的 require 方法返回該模塊的 exports。
4.兩種情況都不滿足,就說明是尚未加載的自定義模塊,會新建一個 Module 對象,將 Module 對象以 filename 為 key 放入緩存中,之后其它模塊引入相同模塊時就可以直接從緩存中取了,最后通過 tryModuleLoad 加載該模塊,并返回該模塊的 module.exports。

Module._load = function (request, parent, isMain) {
    if (parent) {
        debug('Module._load REQUEST %s parent: %S', request, parent.id);
    }

    var filename = Module._resolveFilename(request, parent, isMain);

    var cachedModule = Module._cache[filename];
    if (cachedModule) {
        return cachedModule.exports;
    }

    if (NativeModule.nonInternalExists(filename)) {
        debug('load native module %s', request);
        return NativeModule.require(filename);
    }

    var module = new Module(filename, parent);

    if (isMain) {
        process.mainModule = module;
        module.id = '.';
    }

    Module._cache[filename] = module;

    tryModuleLoad(module, filename);

    return module.exports;
}

Module 對象要注意的點并不多,將 id 初始化為絕對路徑,將 exports 初始化為空對象,還添加了一個和 loaded 屬性,初值為 false,這個屬性值會在 tryModuleLoad 成功調(diào)用完成后變?yōu)?true,表示該模塊加載完成。

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 = []
}

tryModuleLoad 也只是嘗試加載,如果加載失敗,就會從 Module 的緩存中刪除該模塊,而到這里,真正的加載函數(shù) load 方法才冒出水面。

function tryModuleLoad(module, filename) {
    var threw = true;
    try {
        module.load(filename);
        threw = false;
    } finally {
        if (threw) {
            delete Module._cache[filename]
        }
    }
}

同樣先貼上 load 方法的完整代碼再一句句分析。

1.先斷言該模塊未被加載,然后將絕對路徑賦值給該模塊的 filename 屬性(filename 相當(dāng)于和 id 同值)。
2.最核心的代碼其實就是通過 path.extname 獲取擴(kuò)展名,無擴(kuò)展名或 Module._extensions 上午該擴(kuò)展名對應(yīng)的方法時,都會賦值擴(kuò)展名為.js。
3.Module 的_extensions 靜態(tài)屬性是 object 對象,key 為擴(kuò)展名,值為加載對應(yīng)擴(kuò)展名模塊的方法函數(shù)。最后相當(dāng)于通過擴(kuò)展名拿到對應(yīng)函數(shù)執(zhí)行,執(zhí)行完成后,將該模塊加載完成狀態(tài) loaded 改為 true。

Module.prototype.load = function (filename) {
    debug('load %j for module %j', filename, this.id);

    assert(!this.loaded);
    this.filename = filename;
    this.paths = Module._nodeModulePaths(path.dirname(filename));

    var extension = path.extname(filename) || '.js';
    if (!Module._extensions[extension]) extension = '.js';
    Module._extensions[extension](this, filename);
    this.loaded = true;
}

_extensions 共有三種擴(kuò)展名及對應(yīng)的方法:.json、.js、.node。主要分析前兩種,因為.node 一般為 C/C++文件,這里就不涉及相關(guān)知識了。另一方面,.json 和.js 都調(diào)用了 internalModule.stripBOM 方法,作用是剝離 utf8 編碼特有的 BOM 文件頭,也沒必要細(xì)究。

首先是.json 文件,加載流程非常簡單,直接通過 fs 模塊讀取文件為字符串,然后通過 JSON.parse 轉(zhuǎn)為 json 對象賦值給 module.exports 函數(shù)。

Module._extensions['.json'] = function (module, filename) {
    var content = fs.readFileSync(filename, 'utf8');
    try {
        module.exports = JSON.parse(internalModule.stripBOM(content))
    } catch (err) {
        err.message = filename + ':' + err.message;
        throw err;
    }
}

其次是.js,也是先用 fs 模塊讀取為字符串,但注意_compile 編譯完并未做賦值。

Module._extensions['.js'] = function (module, filename) {
    var content = fs.readFileSync(filename, 'utf8');
    module._compile(internalModule.stripBOM(content), filename);
}

_compile 是對讀取完的 js 文件進(jìn)行編譯,表面上函數(shù)很復(fù)雜,但需要關(guān)注的地方并不多,先看 var wrapper = Module.wrap(content),fs 讀取完的字符串作為參數(shù)傳給 wrap 函數(shù)。

Module.prototype._compile = function (content, filename) {
    var contLen = content.length;
    if (contLen >= 2) {
        if (content.charCodeAt(0) === 35 /*#*/ &&
            content.charCodeAt(1) === 33 /*!*/ ) {
            if (contLen === 2) {
                content = '';
            } else {
                var i = 2;
                for (; i < contLen; ++i) {
                    var code = content.charCodeAt(i);
                    if (code === 10 /*\n*/ || code === 13 /*\r*/ )
                        break;
                }
                if (i === contLen)
                    content = '';
                else {
                    content = content.slice(i);
                }
            }
        }
    }

    var wrapper = Module.wrap(content);

    var compiledWrapper = vm.runInThisContext(wrapper, {
        filename: filename,
        lineOffset: 0,
        displayErrors: true
    });

    var inspectorWrapper = null;
    if (process._debugWaitConnect && process._eval == null) {
        if (!resolvedArgv) {
            if (process.argv[1]) {
                resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
            } else {
                resolvedArgv = 'repl';
            }
        }

        if (filename === resolvedArgv) {
            delete process._debugWaitConnect;
            inspectorWrapper = getInspectorCallWrapper();
            if (!inspectorWrapper) {
                const Debug = vm.runInDebugContext('Debug');
                Debug.setBreakPoint(compiledWrapper, 0, 0);
            }
        }
    }

    var dirname = path.dirname(filename);

    var require = internalModule.makeRequireFunction(this);

    var depth = internalModule.requireDepth;
    if (depth === 0) stat.cache = new Map();
    var result;

    if (inspectorWrapper) {
        result = inspectorWrapper(compiledWrapper, this.exports, this.exports, require, this, filename, dirname);
    } else {
        result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
    }
    if (depth === 0) stat.cache = null;
    return result;
};

wrap 函數(shù)的 script 參數(shù)接收字符串后,返回一個被轉(zhuǎn)化為字符串的函數(shù)所包裹的字符串,這樣讀取到的文件內(nèi)容就相當(dāng)于位于函數(shù)體中了?;氐絖compile 函數(shù),緊接著便調(diào)用了 vm.runInThisContext 方法,但需要注意的是,執(zhí)行結(jié)果相當(dāng)于定義了一個函數(shù),并將函數(shù)存在 compiledWrapper 變量中,而并不是調(diào)用這個函數(shù)。

NativeModule.wrap = function (script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];
Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;

忽略掉中間冗余無需關(guān)注的部分,最后需要關(guān)注的只剩 result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname)(自定義模塊都會執(zhí)行 else 情況)。

1.通過 call 方法執(zhí)行 compiledWrapper 所存的函數(shù)。
2.將函數(shù)的 this 指向_load 函數(shù)中 new Module 創(chuàng)建對象的 exports,這也是為什么我們在文件中調(diào)用 this 會輸出暴露的數(shù)據(jù)。
3.將 exports 屬性、require 方法、module 對象自身、module 對象的 filename(同時也是 id)作為文件名、module 對象所在的文件夾路徑 dirname,共同作為函數(shù)參數(shù)傳入。
4.其中 filename 是函數(shù)最開始傳入的參數(shù),dirname 通過 load 函數(shù)的 var dirname = path.dirname(filename)語句獲取。
5.至于 require,可以看到執(zhí)行了一句 var require = internalModule.makeRequireFunction(this),而這句函數(shù)就是給模塊創(chuàng)建一個 require 方法,也就是開始的 require 函數(shù)。
6.注意到,執(zhí)行完的結(jié)果 result 變量雖然 return 了但在_extensions['.js']中并沒有做賦值操作,反而是通過 call 函數(shù),將暴露的數(shù)據(jù)和 new Module 創(chuàng)建對象的 exports 綁定。

最后,再提一遍,在_load 函數(shù)中有 return module.exports,這樣 module.exports 所存的地址層層返回給 require 結(jié)果,就是返回到調(diào)用 require 的模塊中,這樣調(diào)用 require 的模塊也就拿到了被導(dǎo)入模塊所暴露的數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容