Node.js 啟動(dòng)方式:一道關(guān)于全局變量的題目引發(fā)的思考·續(xù)

原文地址:https://xcoder.in/2015/11/27/a-js-problem-about-global-continued/

本文是上文《Node.js 啟動(dòng)方式:一道關(guān)于全局變量的題目引發(fā)的思考》的續(xù)章。

原題回顧

我們還是先回顧下原題吧。

var a = 2;  
function foo(){  
    console.log(this.a);
}

foo();  

上題由我們親愛的小龍童鞋發(fā)現(xiàn)并在我們的 901 群里提問的。

不過在上面一篇文章中,我們講的是在 REPL 和 vm 中有什么事情,但是并沒有解釋為什么在文件模塊的載入形式下,var 并不會(huì)掛載到全局變量去。

其實(shí)原因很簡(jiǎn)單,大家應(yīng)該也都明白,在 Node.js 中,每個(gè)文件相當(dāng)于是一個(gè)閉包,在 require 的時(shí)候被編譯包了起來。

但是具體是怎么樣的呢?雖然網(wǎng)上也有很多答案,我還是決定在這里按上一篇文章的尿性稍微解釋一下。

分析

首先我們還是回到上一篇文章的《Node REPL 啟動(dòng)的沙箱》一節(jié),里面說了當(dāng)啟動(dòng) Node.js 的時(shí)候是以 src/node.js 為入口的。

如果以 REPL 為途徑啟動(dòng)的話是直接啟動(dòng)一個(gè) vm,而此時(shí)的所有根級(jí)變量都在最頂級(jí)的作用域下,所以一個(gè) var 自然會(huì)綁定到 global 下面了。

而如果是以文件,即 $ node foo.js 形式啟動(dòng)的話,它就會(huì)執(zhí)行 src/node.js 里面的另一坨條件分支了。

  // ...
} else if (process.argv[1]) {
  // make process.argv[1] into a full path
  var path = NativeModule.require('path');
  process.argv[1] = path.resolve(process.argv[1]);

  var Module = NativeModule.require('module');

  // ...

  startup.preloadModules();
  if (global.v8debug &&
      process.execArgv.some(function(arg) {
        return arg.match(/^--debug-brk(=[0-9]*)?$/);
      })) {

    var debugTimeout = +process.env.NODE_DEBUG_TIMEOUT || 50;
    setTimeout(Module.runMain, debugTimeout);
  } else {
    // Main entry point into most programs:
    Module.runMain();
  }
} else {
  // ...

從上面的代碼看出,只要是以 $ node foo.js 形式啟動(dòng)的,都會(huì)經(jīng)歷 startup.preloadModules()Module.runMain() 兩個(gè)函數(shù)。

startup.preloadModules()

我們來看看這個(gè)函數(shù)

startup.preloadModules = function() {
  if (process._preload_modules) {
    NativeModule.require('module')._preloadModules(process._preload_modules);
  }
};

實(shí)際上就是執(zhí)行的 lib/module.js 里面的 _preloadModules 函數(shù),并且把這個(gè) process._preload_modules 給傳進(jìn)去。當(dāng)然,前提是有這個(gè) process._preload_modules

process._preload_modules

這個(gè) process._preload_modules 指的就是當(dāng)你在使用 Node.js 的時(shí)候,命令行里面的 --require 參數(shù)。

-r, --require         module to preload (option can be repeated)

代碼在 src/node.cc 里面可考。

// ...
} else if (strcmp(arg, "--require") == 0 ||
           strcmp(arg, "-r") == 0) {
  const char* module = argv[index + 1];
  if (module == nullptr) {
    fprintf(stderr, "%s: %s requires an argument\n", argv[0], arg);
    exit(9);
  }
  args_consumed += 1;
  local_preload_modules[preload_module_count++] = module;
} else if
// ...

如果遇到了 --require 這個(gè)參數(shù),則對(duì)靜態(tài)變量 local_preload_modulespreload_module_count 做處理,把這個(gè)預(yù)加載模塊路徑加進(jìn)去。

待到要生成 process 這個(gè)變量的時(shí)候,再把預(yù)加載模塊的信息放到 process._preload_modules 里面去。

void SetupProcessObject(Environment* env,
                        int argc,
                        const char* const* argv,
                        int exec_argc,
                        const char* const* exec_argv) {
  // ...

  if (preload_module_count) {
    CHECK(preload_modules);
    Local<Array> array = Array::New(env->isolate());
    for (unsigned int i = 0; i < preload_module_count; ++i) {
      Local<String> module = String::NewFromUtf8(env->isolate(),
                                                 preload_modules[i]);
      array->Set(i, module);
    }
    READONLY_PROPERTY(process,
                      "_preload_modules",
                      array);

    delete[] preload_modules;
    preload_modules = nullptr;
    preload_module_count = 0;
  }

  // ...
}

最重要的就是這句

READONLY_PROPERTY(process,
                  "_preload_modules",
                  array);

require('module')._preloadModules

上面我們講了這個(gè) process._preload_modules,然后現(xiàn)在我們說說是如何把 $ node --require bar.js foo.js 給預(yù)加載進(jìn)去的。

接下去我們就要移步到 lib/module.js 文件里面去了。

第 496 行左右的地方有這個(gè)函數(shù)。

Module._preloadModules = function(requests) {
  if (!Array.isArray(requests))
    return;

  // Preloaded modules have a dummy parent module which is deemed to exist
  // in the current working directory. This seeds the search path for
  // preloaded modules.
  var parent = new Module('internal/preload', null);
  try {
    parent.paths = Module._nodeModulePaths(process.cwd());
  }
  catch (e) {
    if (e.code !== 'ENOENT') {
      throw e;
    }
  }
  requests.forEach(function(request) {
    parent.require(request);
  });
};

大概我們能看到,就是以 internal/preload 為 ID 的 Module 對(duì)象來載入這些預(yù)加載模塊。

var parent = new Module('internal/preload', null);
requests.forEach(function(request) {
  parent.require(request);
});

根據(jù)這個(gè)函數(shù)的注釋說明,這個(gè) Module 對(duì)象是一個(gè)虛擬的 Module 對(duì)象,主要是跟非預(yù)加載的那些模塊給隔離或者區(qū)別開來,并且提供一個(gè)模塊搜索路徑。

Module.runMain()

看完上面的說明,我們接下去看看 Module.runMain() 函數(shù)。

這個(gè)函數(shù)還是位于 lib/module.js 文件里面。

Module.runMain = function() {
  // Load the main module--the command line argument.
  Module._load(process.argv[1], null, true);
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();
};

我們看到了就是在這句話中,Module 載入了 process.argv[1] 也就是文件名,自此一發(fā)不可收拾。

Module._load

這個(gè)函數(shù)相信很多人都知道它的用處了,無非就是載入文件,并加載到一個(gè)閉包里面。

這樣一來在文件里面 var 出來的變量就不在根作用域下面了,所以不會(huì)粘到 global 里面去。它的 this 就是包起來的這個(gè)閉包了。

Module._load = function(request, parent, isMain) {
  // ...

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

  // ...
  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;

  module.load(filename);
  return module.exports;
}

上面的代碼首先是根據(jù)傳入的文件名找到真的文件地址,就是所謂的搜索路徑了。比如 require("foo") 就會(huì)分別從 node_modules 路徑等依次查找下來。

我經(jīng)常 Hack 這個(gè) _resolveFilename 函數(shù)來簡(jiǎn)化 require 函數(shù),比如我希望我用 require("controller/foo") 就能直接拿到 ./src/controller/foo.js 文件。有興趣討論一下這個(gè)用法的童鞋可以轉(zhuǎn)到我的 Gist 上查看 Hack 的一個(gè) Demo。

第二步就是我們常說的緩存了。如果這個(gè)模塊之前加載過,那么在 Module._cache 下面會(huì)有個(gè)緩存,直接去取就是了。

第三步就是看看是不是 NativeModule

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

之前的代碼里面其實(shí)也沒少出現(xiàn)這個(gè) NativeModule。那這個(gè) NativeModule 到底是個(gè) shenmegui 呢?

其實(shí)它還是在 Node.js 的入口 src/node.js 里面。

它主要用來加載 Node.js 的一些原生模塊,比如說 NativeModule.require("child_process") 等,也用于一些 internal 模塊的載入,比如 NativeModule.require("internal/repl")

之前代碼的這個(gè)判斷就是說如果判斷要載入的文件是一個(gè)原生模塊,那么就使用 NativeModule.require 來載入。

NativeModule.require
NativeModule.require = function(id) {
  if (id == 'native_module') {
    return NativeModule;
  }

  var cached = NativeModule.getCached(id);
  if (cached) {
    return cached.exports;
  }

  if (!NativeModule.exists(id)) {
    throw new Error('No such native module ' + id);
  }

  process.moduleLoadList.push('NativeModule ' + id);

  var nativeModule = new NativeModule(id);

  nativeModule.cache();
  nativeModule.compile();

  return nativeModule.exports;
};

先看看是否是本身,再看看是否被緩存,然后看看是否合法。接下去就是填充 process.moduleLoadList,最后載入這個(gè)原生模塊、緩存、編譯并返回。

有興趣的同學(xué)可以在 Node.js 中輸出 process.moduleLoadList 看看。

這個(gè) compile 很重要。

NativeModule.prototype.compile

NativeModule 編譯的過程中,大概的步驟是獲取代碼、包裹(Wrap)代碼,把包裹的代碼 runInContext 一遍得到包裹好的函數(shù),然后執(zhí)行一遍就算載入好了。

NativeModule.prototype.compile = function() {
  var source = NativeModule.getSource(this.id);
  source = NativeModule.wrap(source);

  var fn = runInThisContext(source, { filename: this.filename });
  fn(this.exports, NativeModule.require, this, this.filename);

  this.loaded = true;
};

我們往這個(gè) src/node.js 文件這個(gè)函數(shù)的上面幾行看一下,就知道包裹代碼是怎么回事了。

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) {\n',
  '\n});'
];

根據(jù)上面的代碼,我們能知道的就是比如我們一個(gè)內(nèi)置模塊的代碼是:

var foo = require("foo");
module.exports = 1;

那么包裹好的代碼將會(huì)是這樣子的:

(function (exports, require, module, __filename, __dirname) {
var foo = require("foo");
module.exports = 1;
});

這樣一看就明白了這些 requiremoduleexports__filename__dirname 是怎么來了吧。

當(dāng)我們通過 var fn = runInThisContext(source, { filename: this.filename }); 得到了這個(gè)包裹好的函數(shù)之后,我們就把相應(yīng)的參數(shù)傳進(jìn)這個(gè)閉包函數(shù)去執(zhí)行。

fn(this.exports, NativeModule.require, this, this.filename);

這個(gè) this 就是對(duì)應(yīng)的這個(gè) module,自然這個(gè) module 里面就有它的 exportsrequire 函數(shù)就是 NativeModule.require

所以我們看到的在 lib/*.js 文件里面的那些 require 函數(shù),實(shí)際上就是包裹好之后的代碼的 NativeModule.require 了。

所以說實(shí)際上這些內(nèi)置模塊內(nèi)部的根作用域下的 var 再怎么樣高級(jí)也都是在包裹好的閉包里面 var,怎么的也跟 global 搭不著邊。

內(nèi)部原生模塊

通過上面的追溯我們知道了,如果我們?cè)诖a里面使用 require 的話,會(huì)先看看這個(gè)模塊是不是原生模塊。

不過回過頭看一下它的這個(gè)判斷條件:

if (NativeModule.nonInternalExists(filename)) {
  // ...
}

如果是原生模塊并且不是原生內(nèi)部模塊的話。

那是怎么區(qū)分原生模塊和內(nèi)部原生模塊呢?

我們?cè)賮砜纯催@個(gè) NativeModule.nonInternalExists(filename) 函數(shù)。

NativeModule.nonInternalExists = function(id) {
  return NativeModule.exists(id) && !NativeModule.isInternal(id);
};

NativeModule.isInternal = function(id) {
  return id.startsWith('internal/');
};

上面的代碼是去除各種雜七雜八的條件之后的一種情況,別的情況還請(qǐng)各位童鞋自行看 Node.js 源碼。

也就是說我們?cè)谖覀冏约旱拇a里面是請(qǐng)求不到 Node.js 源碼里面 lib/internal/*.js 這些文件的——因?yàn)樗鼈儽簧厦娴倪@個(gè)條件分支給過濾了。(比如 require("internal/module") 在自己的代碼里面是無法運(yùn)行的)

注意: 不過有一個(gè)例外,那就是 require("internal/repl")。詳情可以參考這個(gè) Issue這段代碼

Module.prototype.load

解釋完了上面的 NativeModule 之后,我們要就上面 Module._load 里面的下一步 module.load 也就是 Module.prototype.load 做解析了。

Module.prototype.load = function(filename) {
  // ...

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

做了一系列操作之后得到了真·文件名,然后判斷一下后綴。如果是 ".js" 的話執(zhí)行 Module._extensions[".js"] 這個(gè)函數(shù)去編譯代碼,如果是 ".json" 則是 Module._extensions[".json"]

這里我們略過 JSON 和 C++ Addon,直奔 Module._extensions[".js"]

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

它也很簡(jiǎn)單,就是奔著 _compile 去的。

Module.prototype._compile

先上代碼

Module.prototype._compile = function(content, filename) {
  var self = this;
  // remove shebang
  content = content.replace(shebangRe, '');

  function require(path) {
    return self.require(path);
  }

  require.resolve = function(request) {
    return Module._resolveFilename(request, self);
  };

  require.main = process.mainModule;

  // Enable support to add extra extension types
  require.extensions = Module._extensions;

  require.cache = Module._cache;

  var dirname = path.dirname(filename);

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = runInThisContext(wrapper,
                                      { filename: filename, lineOffset: -1 });

  // ...

  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

感覺流程上跟 NativeModule 的編譯相似,不過這里是事先準(zhǔn)備好要在載入的文件里面用的 require 函數(shù),以及一些 require 的周邊。

接下去就是用 Module.wrap 來包裹代碼了,包裹完之后把得到的函數(shù)用參數(shù) self.exports, require, self, filename, dirname 去執(zhí)行一遍,就算是文件載入完畢了。

最后回到之前載入代碼的那一刻,把載入完畢得到的 module.exportsreturn 出去就好了。

Module.wrap

這個(gè)就不用說了。

在 lib/module.js 的最頂端附近有這么幾行代碼。

Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;
Module._debug = util.debuglog('module');

一切豁然開朗了吧。

NativeModule 的代碼都逃不開被之前說的閉包所包裹,那么你自己寫的 JS 文件當(dāng)然也會(huì)被 NativeModule.wrap 所包裹。

那么你在代碼根作用域申明的函數(shù)實(shí)際上在運(yùn)行時(shí)里面已經(jīng)被一個(gè)閉包給包住了。

以前可能很多同學(xué)只知道是被閉包包住了,但是包的方法、流程今天算是解析了一遍了。

(function (exports, require, module, __filename, __dirname) {
var a = 2;
function foo(){  
    console.log(this.a);
}

foo();
});

這個(gè) var a 怎么也不可能綁到 global 去啊。

Module.prototype.require

雖然我們上面講得差不多了,可能很多童鞋也厭煩了。

不過該講完的還是得講完。

我們?cè)谖覀冏约何募杏玫?require 在上一節(jié)里面有提到過,傳到我們閉包里面的 require 實(shí)際上是長(zhǎng)這樣的:

function require(path) {
  return self.require(path);
}

所以實(shí)際上就是個(gè) Module.prototype.require

我們?cè)倏纯?a target="_blank" rel="nofollow">這個(gè)函數(shù)。

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this);
};

一下子又繞回到了我們一開始的 Module._load

所以基本上就差不多到這過了。

REPL vs 文件啟動(dòng)

最后我們?cè)冱c(diǎn)一下,或者說回顧一下吧。

REPL 啟動(dòng)的時(shí)候 Node.js 是開了個(gè) vm 直接讓你跑,并沒有把代碼包在一個(gè)閉包里面,所以再根作用域下的變量會(huì) Biu 一下貼到 global 中去。

而文件啟動(dòng)的時(shí)候,會(huì)做本文中說的一系列事情,然后就會(huì)把各文件都包到一個(gè)閉包去,所以變量就無法通過這種方式來貼到 global 去了。

不過這種二義性會(huì)在 "use strict"; 中戛然而止。

珍愛生命,use strict

小結(jié)

本文可能很多童鞋看完后悔覺得很坑——JS 為什么有那么多二義性那么坑呢。

其實(shí)不然,主要是可能很多人對(duì) Node.js 執(zhí)行的機(jī)制不是很了解。

本文從小龍拋出的一個(gè)簡(jiǎn)單問題進(jìn)入,然后淺入淺出 Node.js 的一些執(zhí)行機(jī)制什么的,希望對(duì)大家還是有點(diǎn)幫助,更何況我在意的不是問題本身,而是分析的這個(gè)過程。

番外

以下均為臆想。

小龍: 喂喂喂,我就問一個(gè)簡(jiǎn)單的小破題目,你至于嘛!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容