經過源碼解析 Node.js 啓動時第一個執行的 js 文件:bootstrap_node.js

你們可能會好奇,在 Node.js 啓動後,第一個執行的 JavaScript 文件會是哪一個?它具體又會幹些什麼事?javascript

一步步來看,翻開 Node.js 的源碼,不難看出,入口文件在 src/node_main.cc 中,主要任務爲將參數傳入 node::Start 函數:java

// src/node_main.cc
// ...

int main(int argc, char *argv[]) {
  setvbuf(stderr, NULL, _IOLBF, 1024);
  return node::Start(argc, argv);
}

node::Start 函數定義於 src/node.cc 中,它進行了必要的初始化工做後,會調用 StartNodeInstancenode

// src/node.cc
// ...

int Start(int argc, char** argv) {
    // ...
    NodeInstanceData instance_data(NodeInstanceType::MAIN,
                                   uv_default_loop(),
                                   argc,
                                   const_cast<const char**>(argv),
                                   exec_argc,
                                   exec_argv,
                                   use_debug_agent);
    StartNodeInstance(&instance_data);
}

而在 StartNodeInstance 函數中,又調用了 LoadEnvironment 函數,其中的 ExecuteString(env, MainSource(env), script_name); 步驟,便執行了第一個 JavaScript 文件代碼:c++

// src/node.cc
// ...
void LoadEnvironment(Environment* env) { 
  // ...
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
  // ...
}

static void StartNodeInstance(void* arg) {
  // ...
  {
      Environment::AsyncCallbackScope callback_scope(env);
      LoadEnvironment(env);
  }
  // ...
}

// src/node_javascript.cc
// ...

Local<String> MainSource(Environment* env) {
  return String::NewFromUtf8(
      env->isolate(),
      reinterpret_cast<const char*>(internal_bootstrap_node_native),
      NewStringType::kNormal,
      sizeof(internal_bootstrap_node_native)).ToLocalChecked();
}

其中的 internal_bootstrap_node_native ,即爲 lib/internal/bootstrap_node.js 中的代碼。(注:不少之前的 Node.js 源碼分析文章中,所寫的第一個執行的 JavaScript 文件代碼爲 src/node.js ,但這個文件在 Node.js v5.10 中已被移除,並被拆解爲了 lib/internal/bootstrap_node.js 等其餘 lib/internal 下的文件,PR 爲: https://github.com/nodejs/node/pull/5103git

正文

做爲第一段被執行的 JavaScript 代碼,它的歷史使命免不了就是進行一些環境和全局變量的初始化工做。代碼的總體結構很簡單,全部的初始化邏輯都被封裝在了 startup 函數中:github

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
  }
  // ...
  startup();
});

而在 startup 函數中,邏輯能夠分爲四塊:bootstrap

  • 初始化全局 process 對象上的部分屬性 / 行爲緩存

  • 初始化全局的一些 timer 方法app

  • 初始化全局 console 等對象dom

  • 開始執行用戶執行指定的 JavaScript 代碼

讓咱們一個個來解析。

初始化全局 process 對象上的部分屬性 / 行爲

添加 processuncaughtException 事件的默認行爲

在 Node.js 中,若是沒有爲 process 上的 uncaughtException 事件註冊監聽器,那麼該事件觸發時,將會致使進程退出,這個行爲即是在 startup 函數裏添加的:

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    setupProcessFatal();
  }
  // ...

  function setupProcessFatal() {

    process._fatalException = function(er) {
      var caught;
      // ...
      if (!caught)
        caught = process.emit('uncaughtException', er);

      if (!caught) {
        try {
          if (!process._exiting) {
            process._exiting = true;
            process.emit('exit', 1);
          }
        } catch (er) {
        }
      } 
      // ...
      return caught;
    };
  }
});

邏輯十分直白,使用到了 EventEmitter#emit 的返回值來判斷該事件上是否有註冊過的監聽器,並最終調用 c++ 的 exit() 函數退出進程:

// src/node.cc
// ...

void FatalException(Isolate* isolate,
                    Local<Value> error,
                    Local<Message> message) {
  // ...
  Local<Value> caught =
      fatal_exception_function->Call(process_object, 1, &error);
      
  // ...
  if (false == caught->BooleanValue()) {
    ReportException(env, error, message);
    exit(1);
  }
}

根據 Node.js 在啓動時所帶的某些參數,來調整 processwarning 事件觸發時的行爲

具體來講,這些參數是:--no-warnings--no-deprecation--trace-deprecation--throw-deprecation。這些參數的有無信息,會先被掛載在 process 對象上:

// src/node.cc
// ...

  if (no_deprecation) {
    READONLY_PROPERTY(process, "noDeprecation", True(env->isolate()));
  }

  if (no_process_warnings) {
    READONLY_PROPERTY(process, "noProcessWarnings", True(env->isolate()));
  }

  if (trace_warnings) {
    READONLY_PROPERTY(process, "traceProcessWarnings", True(env->isolate()));
  }

  if (throw_deprecation) {
    READONLY_PROPERTY(process, "throwDeprecation", True(env->isolate()));
  }

而後根據這些信息,控制行爲:

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
    NativeModule.require('internal/process/warning').setup();
  }
  // ...
  startup();
});
// lib/internal/process/warning.js
'use strict';

const traceWarnings = process.traceProcessWarnings;
const noDeprecation = process.noDeprecation;
const traceDeprecation = process.traceDeprecation;
const throwDeprecation = process.throwDeprecation;
const prefix = `(${process.release.name}:${process.pid}) `;

exports.setup = setupProcessWarnings;

function setupProcessWarnings() {
  if (!process.noProcessWarnings) {
    process.on('warning', (warning) => {
      if (!(warning instanceof Error)) return;
      const isDeprecation = warning.name === 'DeprecationWarning';
      if (isDeprecation && noDeprecation) return;
      const trace = traceWarnings || (isDeprecation && traceDeprecation);
      if (trace && warning.stack) {
        console.error(`${prefix}${warning.stack}`);
      } else {
        var toString = warning.toString;
        if (typeof toString !== 'function')
          toString = Error.prototype.toString;
        console.error(`${prefix}${toString.apply(warning)}`);
      }
    });
  }
  // ...
}

具體行爲的話,文檔中已經有詳細說明,邏輯總結來講,就是按需將警告打印到控制檯,或者按需拋出特定的異常。其中 NativeModule 對象爲 Node.js 在當前的函數體的局部做用域內,實現的一個最小可用的模塊加載器,具備緩存等基本功能。

process 添加上 stdin, stdoutstderr 屬性

一般爲 tty.ReadStream 類和 tty.WriteStream 類的實例:

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
    NativeModule.require('internal/process/stdio').setup();
  }
  // ...
  startup();
});
// lib/internal/process/stdio.js
// ...

function setupStdio() {
  var stdin, stdout, stderr;

  process.__defineGetter__('stdout', function() {
    if (stdout) return stdout;
    stdout = createWritableStdioStream(1);
    // ...
    return stdout
  }

  process.__defineGetter__('stderr', function() {
    if (stderr) return stderr;
    stderr = createWritableStdioStream(2);
    // ...
    return stderr;
  });

  process.__defineGetter__('stdin', function() {
    if (stdin) return stdin;

    var tty_wrap = process.binding('tty_wrap');
    var fd = 0;

    switch (tty_wrap.guessHandleType(fd)) {
      case 'TTY':
        var tty = require('tty');
        stdin = new tty.ReadStream(fd, {
          highWaterMark: 0,
          readable: true,
          writable: false
        });
        break;
      // ...
    }
    return stdin;
  }
}

function createWritableStdioStream(fd) {
  var stream;
  var tty_wrap = process.binding('tty_wrap');

  // Note stream._type is used for test-module-load-list.js

  switch (tty_wrap.guessHandleType(fd)) {
    case 'TTY':
      var tty = require('tty');
      stream = new tty.WriteStream(fd);
      stream._type = 'tty';
      break;
    // ...    
  }
  // ...
}

process 添加上 nextTick 方法

具體的作法即是將註冊的回調推動隊列中,等待事件循環的下一次 Tick ,一個個取出執行:

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
    NativeModule.require('internal/process/next_tick').setup();
  }
  // ...
  startup();
});
// lib/internal/process/next_tick.js
'use strict';

exports.setup = setupNextTick;

function setupNextTick() {
  var nextTickQueue = [];
  // ...
  var kIndex = 0;
  var kLength = 1;

  process.nextTick = nextTick;
  process._tickCallback = _tickCallback;

  function _tickCallback() {
    var callback, args, tock;

    do {
      while (tickInfo[kIndex] < tickInfo[kLength]) {
        tock = nextTickQueue[tickInfo[kIndex]++];
        callback = tock.callback;
        args = tock.args;
        _combinedTickCallback(args, callback);
        if (1e4 < tickInfo[kIndex])
          tickDone();
      }
      tickDone();
    } while (tickInfo[kLength] !== 0);
  }

  function nextTick(callback) {
    if (typeof callback !== 'function')
      throw new TypeError('callback is not a function');
    if (process._exiting)
      return;

    var args;
    if (arguments.length > 1) {
      args = new Array(arguments.length - 1);
      for (var i = 1; i < arguments.length; i++)
        args[i - 1] = arguments[i];
    }

    nextTickQueue.push(new TickObject(callback, args));
    tickInfo[kLength]++;
  }
}
// ...

process 添加上 hrtime, kill, exit 方法

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
    _process.setup_hrtime();
    _process.setupKillAndExit();
  }
  // ...
  startup();
});

這些功能的核心實現也重度依賴於 c++ 函數:

  • hrtime 方法依賴於 libuv 提供的 uv_hrtime() 函數

  • kill 方法依賴於 libuv 提供的 uv_kill(pid, sig) 函數

  • exit 方法依賴於 c++ 提供的 exit(code) 函數

初始化全局的一些 timer 方法和 console 等對象

這些初始化都乾的十分簡單,直接賦值:

// lib/internal/bootstrap_node.js
'use strict';

(function(process) {
  function startup() {
    // ...
    setupGlobalVariables();
    if (!process._noBrowserGlobals) {
      setupGlobalTimeouts();
      setupGlobalConsole();
    }
    
    function setupGlobalVariables() {
      global.process = process;
      // ...
      global.Buffer = NativeModule.require('buffer').Buffer;
      process.domain = null;
      process._exiting = false;
    }
    
    function setupGlobalTimeouts() {
      const timers = NativeModule.require('timers');
      global.clearImmediate = timers.clearImmediate;
      global.clearInterval = timers.clearInterval;
      global.clearTimeout = timers.clearTimeout;
      global.setImmediate = timers.setImmediate;
      global.setInterval = timers.setInterval;
      global.setTimeout = timers.setTimeout;
    }

    function setupGlobalConsole() {
      global.__defineGetter__('console', function() {
        return NativeModule.require('console');
      });
    }
  }
  // ...
  startup();
});

值得注意的一點是,因爲 console 是經過 __defineGetter__ 賦值給 global 對象的,因此在嚴格模式下給它賦值將會拋出異常,而非嚴格模式下,賦值將被忽略。

開始執行用戶執行指定的 JavaScript 代碼

這一部分的邏輯已經在以前的文章中有所闡述,這邊就再也不重複說明啦。

最後

仍是再次總結下:

  • lib/internal/bootstrap_node.js 中的代碼 爲 Node.js 執行後第一段被執行的 JavaScript 代碼,從 src/node.cc 中的 node::LoadEnvironment 被調用

  • lib/internal/bootstrap_node.js 主要進行了一些初始化工做:

    • 初始化全局 process 對象上的部分屬性 / 行爲

      • 添加接收到 uncaughtException 事件時的默認行爲

      • 根據 Node.js 啓動時參數,調整 warning 事件的行爲

      • 添加上 stdinstdoutstderr 屬性

      • 添加上 nextTickhrtimeexit 方法

    • 初始化全局的一些 timer 方法

    • 初始化全局 console 等對象

    • 開始執行用戶執行指定的 JavaScript 代碼

參考

相關文章
相關標籤/搜索