Link and Motivation Developers' Blog

リンクアンドモチベーションの開発者ブログです

ブラウザ JavaScript / Node.js の仕組みを知ろう! ~トラブルに迅速に立ち向かえる様に

こんにちは。リンクアンドモチベーション SRE グループの川津と申します!

弊社ではフロントエンドは勿論、開発環境の様々な所で JavaScript (Node.js) を利用しています。 JavaScript は歴史的経緯上 様々なランタイム&実行環境が存在し、仕組みが理解しづらいので書いてみました!


はじめに

近年 JavaScript の需要は増し、Web Application のほとんどは JavaScript を使って動いているのではないかと思います。

もともと JavaScriptDOM API (Document Object Model、HTML を JavaScript でから操作できる) の為にありました。DOM API を更に使いやすくした jQuery 等が流行りましたね。

しかし JavaScript の活用範囲は拡大し、現在では以下の様々な用途で利用されます。

例えばフロントエンド開発をするとき、ローカルマシンに Node.js をインストールして使いますが、Node.js では使えるがブラウザ JavaScript では使えない API (ファイル入出力等) があります。同じ JavaScript の筈なのに何故でしょう?

つまり、どこまでが JavaScript なのでしょうか?

巷でよく聞く ECMAScript (≒ JavaScript ?) とは何なのかを追っていきましょう。

Web ブラウザの仕組み (標準仕様と実装)

まずは JavaScript 実行のベースとなっている Web ブラウザの仕組みについて追っていきましょう。

構成要素

Web ブラウザの仕組みを図に書き出してみると、おおよそ以下の様になります。

以下は Google Chrome での例です。 他ブラウザでも基本的なアーキテクチャはほぼ同じですが、一部異なる箇所はあります。

以下は図中の構成要素の説明です。

# 構成要素 実装系 説明
1 ユーザーインターフェース Google Chrome ウインドウやタブ、URL バー等、デスクトップアプリケーションとしての UI 部分です。 (つまり OS 毎に実装が異なります)
2 ブラウザ エンジン Google Chrome UI 部分 (OS 上のプロセス) と、レンダリングエンジン間の橋渡しをします。
3 レンダリング エンジン Blink 主に HTML の画面描画をしますが、JavaScript エンジンの初期化・起動等、様々な処理をします。
4 JavaScript エンジン V8 ECMAScript (ECMA-262) 仕様に準拠したエンジンで、JavaScript コンテキスト (実行空間) の生成と、ユーザー JavaScript コードの実行をします。

(1) ユーザーインターフェース

ウインドウやメニュー等、アプリケーション (プロセス) としての UI 部分です。 この部分の実装は OS によって異なります。

(2) ブラウザ エンジン

ブラウザエンジンは、UI 部分 (メインプロセス) と、レンダラプロセスとの橋渡しを行います。

Google Chrome では、ウィンドウやタブを開く度にレンダリング処理をするサブプロセスが立ち上がります。

Chronium 系ブラウザではマルチプロセス動作をしますが、他ブラウザはそうではありません。

(3) レンダリング エンジン

Google Chrome では、レンダリングエンジンに Blink を採用しています。

Blink は、Safari 等で使われている WebKit の派生プロジェクトです。

レンダリングエンジンは主に HTML/CSS の描画処理をします。 (本書では描画については述べません) 後述する「JavaScript エンジン」への実行指示もレンダリングエンジンが行います。

(4) JavaScript エンジン

※次章で詳しく説明をします。

JavaScript コンテキストと Web API

ブラウザのウインドウやタブでページ遷移 (URL の変更) が発生すると、レンダリングエンジンから JavaScript エンジンへ 新しい JavaScript コンテキストの生成 が指示されます。

HTML 中に含まれる <script> タグで入力したユーザー JavaScript は、この生成されたコンテキスト上に取り込まれ実行されます。

図解

# 処理 説明
1 JavaScript エンジン (V8) は、新しいコンテキスト (JS 実行空間) を生成します JavaScript の標準的な構文、オブジェクトが利用可能になります
2 Web IDL で定義された Web API がコンテキスト内に JavaScript オブジェクトとして定義される レンダリングエンジンが提供する Web API が利用可能になります
3 ユーザー JavaScript コードを、コンテキスト内で実行する

V8 は ECMASCRIPT に準拠

ここで最も重要な事は JavaScript エンジン (V8) は ECMASCRIPT (ECMA-262) 仕様の実装系である という事です。

例えば、ブラウザ JavaScript で良く使われる setTimeout()console.log() は、実は ECMASCRIPT 仕様ではありません。 これらの API 実装はブラウザが提供しており、 Web API 仕様に当たります。

JS 仕様 JavaScript 構文/API
JavaScript (ECMASCRIPT) 標準組込みオブジェクト Array, Number, Promise, JSON
式と演算子 +, -, this, function
文と宣言 if...else, for...in
Web API Console API console.log()
Document Document.getElementById()

↑ に挙げたのは一例で、網羅はしてません。

API がどの仕様に準拠しているか?

MDNパンくずリストを見ると、その API が何の仕様に基づいているか分かります。 例えば PromiseJavaScript (ECMAScript) 仕様です。

関数 setTimeout()Web API 仕様であることが分かります。

他ブラウザでは?

前章では Google Chrome を例に説明をしましたが、他ブラウザでも基本的な仕組みは同じで、実装系 (BlinkV8 等) が以下の通り異なります。

ブラウザ レンダリングエンジン JavaScript エンジン 補足説明
Google Chrome Blink V8 Chromium ベース
Mozilla FireFox Gecko SpiderMonkey Mozilla がメインでメンテ
Safari WebKit JavaScriptCore Apple がメインでメンテ
Microsoft Edge Blink V8 Chromium ベースに移行
Microsoft Edge ※旧版 EdgeHTML Chakra 2021年3月9日にサポートを終了
Internet Explorer Trident Chakra 2022年6月16日にサポートを終了

モバイルもありますが、割愛。

Node.js の仕組み (標準仕様と実装)

ここまでで Web ブラウザの仕組みを追ってみました。 では Node.js はどういう仕組になっているのでしょうか?

Web ブラウザの仕組みを知っている事が Node.js の理解にも繋がります。

図解

# 処理 説明
1 JavaScript エンジン (V8) は、新しいコンテキスト (JS 実行空間) を生成します JavaScript の標準的な構文、オブジェクトが利用可能になります
2 Node.js 独自のコアモジュール (Node.js API) をコンテキスト中に定義する コアモジュール (JavaScript) コードは Node.js リポジトリlib/ 配下にあります。

実際の API 処理の実態は Node.js の C++ コード側にあり、lib/ 配下の JavaScript コードは該当する C++ 実装の Binding を取得してコード呼び出しをしています。
3 ユーザー JavaScript コードを、コンテキスト内で実行する

Node.js も Chrome と同じ V8 エンジン

Node.jsJavaScript エンジンに V8 を採用しています。従って標準的な JavaScript 仕様 (ECMASCRIPT) の範囲では Google Chrome と同じ様に動作します。

しかし Web ブラウザの章で述べた通り setTimeout()console.log()APIECMASCRIPT 仕様に含まれていませんが、 Node.js ではこれらの API を問題なく使えます。何故でしょうか?

その答えは Node.js 独自の Node.js API (コアモジュール) として、これらの API が実装されているからです。

つまり上記の API は Web ブラウザでの Web API 仕様との互換性を保つために追加されたものですが、 あくまで Node.js 独自の API であり、Web API 仕様を準拠しているわけではありません。

setTimeout() を使用して、 指定したミリ秒後に コードの実行をスケジュールすることができます。 この関数はブラウザの JavaScript API の window.setTimeout()に似ていますが、 コードの文字列を渡して実行することはできません。

アップデートによる影響

Node.js をアップデートする際は前述の構造から次の2点が影響範囲であると分かります。

  1. JavaScript エンジン V8 の変更
  2. Node.js (コアモジュール) の変更

ECMAScript 仕様は後方互換性を最大限尊重します。 従って、JavaScript エンジン (V8) の変更による既存のコードへの影響は比較的少ないと言えます。

例えば Chrome 等の Web ブラウザはバックグラウンドで秘密裏にアップデートされています。

一方で、Node.js (コアモジュール) の変更では、破壊的変更が加えられる事は珍しくありません。

実際に Node.js のコードを追ってみよう!

ここからはかなりニッチな内容になるので興味ある方だけ ^^;

ユーザー JavaScript コードから fs.chmod() を呼んだ場合に、この API の実態 (C++ 実装) は Node.js ソースコード の何処にあるのか?を追います。

お題 → fs.chmod()

Node.js 公式の API Documentation に fs.chmod() の記載があります。

これをお題にコードを追いましょう。

import { chmod } from 'fs';

chmod('my_file.txt', 0o775, (err) => {
  if (err) throw err;
  console.log('Node.js 公式の chmod 使用例です.');
});

fs モジュールのフロント実装.

ユーザースクリプトimport { chmod } from 'fs'; しているモジュールの実態は lib/fs.js にあります。

この function の JS 実装はたった6行しか無く、最終行 L1315binding.chmod()C++ 側の実装を呼んでいる箇所になります。

function chmod(path, mode, callback) {
  path = getValidatedPath(path);
  mode = parseFileMode(mode, 'mode');
  callback = makeCallback(callback);

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.chmod(pathModule.toNamespacedPath(path), mode, req);  // ← ここがキモ
}

この binding オブジェクトはファイルの先頭 L59 で生成されています。

const binding = internalBinding('fs');

JavaScriptC++ 実装への Binding の仕組み.

Node.js の JavaScript / C++ Binding の仕組み自体は README.md に書いてあります。

書いてある事は難しいが、要は ↓ という事です。

C++ 側で NODE_MODULE_CONTEXT_AWARE_INTERNAL(モジュール名, Initialize関数) 定義したモジュールは、JavaScript 側で internalBinding('モジュール名') で取得できるよ

実際に lib/fs.js JavaScript モジュールに該当する C++ 実装は src/node_file.cc ファイルになります。

C++ src/node_file.cc ファイルの最終行 L2535バインディングの定義があります。

NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)

JavaScript fs.chmod() に該当する C++ 実装は、このコード中の以下の関数 node::fs::Chmod() L2114 です。

/* fs.chmod(path, mode);
 * Wrapper for chmod(1) / EIO_CHMOD
 */
static void Chmod(const FunctionCallbackInfo<Value>& args) {
  // 省略...
}

JavaScript APIchmod とこの C++ 関数 node::fs::Chmod() との紐付けは同ファイルの node::fs::Initialize() 関数内 L2439 にあります。

  env->SetMethod(target, "chmod", Chmod);
  env->SetMethod(target, "fchmod", FChmod);

  env->SetMethod(target, "chown", Chown);
  env->SetMethod(target, "fchown", FChown);
  env->SetMethod(target, "lchown", LChown);