実装レベルで見る JavaScript の実行 (V8 & Chromium)
JavaScript の非同期を理解する記事の第 4 弾です。
ここまで JavaScript の実行時に何が起こっているかの概要をざっくりと理解しました。
今回は、実際の実装レベルの内容を少しみていきます。
さてここまでの記事で、以下のことを学んできました。
前回の記事: ブラウザでの JavaScript の実行
- WEB ブラウザで実行される JavaScript は JavaScript エンジン と Web ブラウザの両方が連携して初めて実現してるということ。
- ブラウザは Event Loop というものを使って、複数の Queue を管理しており、その中で JavaScript の実行も管理していること。
- Promise オブジェクトについては
microtask Queue
という専用の Queue を使って、高い優先度にて実行されること。
役割 | オーナー | 範囲 |
---|---|---|
「microtask」 という概念と基本的な動作 | ECMAScript | Promise などの標準ライブラリの動作の一部として定義し、処理モデルを規定 |
イベントループの概念定義と全体設計 | WHATWG HTML | HTML Living Standard の「8.1.5 Event loops」で定義、実行順序とアルゴリズムを明確化 |
microtask queue の実行タイミング | WHATWG HTML | HTML Living Standard の「8.1.7 Microtask queuing」で定義 |
タスク優先度と実行順序の規定 | WHATWG HTML | マクロタスク、マイクロタスク、レンダリングの実行順序と優先度を定義 |
microtask queue の実際の実装 | JavaScript エンジン(例:V8) | キュー構造・処理順序の内部実装 |
イベントループの実際の実装と実行 | ブラウザエンジン(例:Blink) | WHATWG 仕様に基づいたイベントループの実装、JavaScript エンジンとの連携を担当 |
今回は、特に新しい概念は出てきませんが、実際にどのように実装されているかを少しだけみてみたいと思います。
v8 のコードを読んでみる
第2弾の記事で、JavaScript エンジンを理解するために、v8 を利用しました。
今回は、v8 のコード内を少しだけみることで、Queue の理解を深めたいと思います。
v8 のコードレポジトリ
まずは include/v8-platform.h
をみていきます。ここには v8 のコード内で使用されている単語の定義が書いています。
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
...
/**
* Schedules a task to be invoked by this TaskRunner. The TaskRunner
* implementation takes ownership of |task|. The |task| cannot be nested
* within other task executions.
*
* Tasks which shouldn't be interleaved with JS execution must be posted with
* |PostNonNestableTask| or |PostNonNestableDelayedTask|. This is because the
* embedder may process tasks in a callback which is called during JS
* execution.
*
* In particular, tasks which execute JS must be non-nestable, since JS
* execution is not allowed to nest.
*
* Requires that |TaskRunner::NonNestableTasksEnabled()| is true.
*
* Embedders should override PostNonNestableTaskImpl instead of this.
*/
void PostNonNestableTask(
std::unique_ptr<Task> task,
const SourceLocation& location = SourceLocation::Current()) {
PostNonNestableTaskImpl(std::move(task), location);
}
...
これは PostTask メソッドに関する説明です。
平たく言うと、TaskRunner によって実行されるタスクが、どのように扱われるべきかを示しています。特に JavaScript 実行中のタスクについては、「ネストできない(=途中で割り込まれては困る)」ため、PostNonNestableTask
または PostNonNestableDelayedTask
を使う必要があります。
これは、ブラウザや Node.js のような環境では、JavaScript 実行中に callback を通じて別のタスクが実行されてしまう可能性があるからです。その結果、意図しないタイミングでの割り込みが起こり、状態の不整合やバグの原因になることがありますし、言語仕様として許されていないことです。
このような理由から、JavaScript はネストできないタスクを使うように書いてあります。
似たような Task として PostNonNestableDelayedTask
もあります。これは、指定した秒数だけ遅れて実行される Task です。
次に include/v8-context.h
ファイルを見ていきます。
これは、実行コンテキストのヘッダーファイルです。このファイルをみると、実際に v8 では、Execution Context インスタンスにさまざまなデータが含まれることがわかります。
include/v8-microtask-queue.h
, src/execution/microtask-queue.cc
をみてみるとMicrotaskQueue
の概要がわかるかと思います。ここでは円環キューを使って、Task の管理をしています。RunMicrotasks
メソッドで実際に Task を実行しています。
そして v8 のコード内にて、"Event Loop"や"他のキュー"を探してみますが、これらは見つかりません。ここまで学んできた通り、これらは実行環境である Web ブラウザ側 で定義、実装されている機能です。そのため、JavaScript エンジンの中では、これらの機能は定義されていません。
さて、それでは次にブラウザの Chromium のコードを見ていきたいと思います。
Chromium(Blink) のコードを見てみる
Chromium のコードレポジトリ
Chromium はオープンソースのウェブブラウザ基盤であり、Google Chrome と Edge の基盤となっています。Chromium のコードでは、主にイベントループの実装レベルの概要と、TaskQueue と MicrotaskQueue の実装を見ていきます。blink
は Chromium にて使用されているブラウザエンジンです。ブラウザエンジンのblink
では、実際のブラウザの描画を担っています。HTML や CSS の解析や、DOM の操作などが行われています。
JavaScript エンジンとの連携も行われています。
Chromium(blink) での Event loop の実装
third_party/blink/renderer/platform/scheduler/common/event_loop.cc
の EventLoop
クラスを見てみます。ここではMicrotask
の実行の実装が行われています。
EventLoop のコンストラクタ
- V8 の isolate とマイクロタスクキューを受け取り、イベントループの初期状態を設定
EventLoop::EventLoop(EventLoop::Delegate* delegate,
v8::Isolate* isolate,
std::unique_ptr<v8::MicrotaskQueue> microtask_queue)
: delegate_(delegate),
isolate_(isolate),
microtask_queue_(std::move(microtask_queue)) {
DCHECK(isolate_);
DCHECK(delegate);
DCHECK(microtask_queue_);
microtask_queue_->AddMicrotasksCompletedCallback(
&EventLoop::RunEndOfCheckpointTasks, this);
}
マイクロタスクの登録
- V8 のマイクロタスクキューに実行関数を登録
- Promise などの非同期処理がここを通じて登録される
void EventLoop::EnqueueMicrotask(base::OnceClosure task) {
pending_microtasks_.push_back(std::move(task));
microtask_queue_->EnqueueMicrotask(isolate_, &EventLoop::RunPendingMicrotask, this);
}
マイクロタスクチェックポイント
- マイクロタスク実行の許可を確認
- V8 のマイクロタスクキューを実行
void EventLoop::PerformMicrotaskCheckpoint() {
if (ScriptForbiddenScope::IsScriptForbidden())
return;
microtask_queue_->PerformCheckpoint(isolate_);
}
マイクロタスクの実行
- キューの先頭からタスクを取得、実行
// static
void EventLoop::RunPendingMicrotask(void* data) {
TRACE_EVENT0("renderer.scheduler", "RunPendingMicrotask");
auto* self = static_cast<EventLoop*>(data);
base::OnceClosure task = std::move(self->pending_microtasks_.front());
self->pending_microtasks_.pop_front();
std::move(task).Run();
}
これまでの記事で見てきた内容が、実際に実装されているところを見ると感動します。
実際にはブラウザは、JavaScript 実行以外にもさまざまな処理を担っており、複数のキューを管理しています。ブラウザが管理するさまざまなキューは、以下のディレクトリに実装されています。
タスクキューの管理: base/task/sequence_manager/sequence_manager_impl.cc
実際のコードはぜひ見てもらえればと思いますが、ざっくりと以下のようにタスクが実行されています。
まとめ
JavaScript エンジンとブラウザの実装を見てみることで、両者の役割を再確認することができたと思います。
結果として、
- JavaScript エンジンとブラウザの役割
- 実際の実装が行われているソースコードの場所
- 実装のほんの一端
について、理解できました。
前回までの記事では、実行モデルや仕様など抽象的な概念が多かったですが、実際にコードを読んでみると、その実装がどのようになっているかがわかり、より理解が深まったと思います。興味を持った方はぜひ、Chromium について自分でも調べてみてもらうのが良いと思います。
次回
この記事では これまでの記事で解説した内容を踏まえて、「JavaScript 実行モデルの実装」について解説しました。
ここまでの話は概念や仕様の話ばかりでしたが、次回からは業務でも見かけるような"非同期処理"の実際のコードがメインとなっていきます。まずは Callback
関数について学んでいければと思います。
- そもそも Callback 関数って何?
- Callback 関数はなぜ非同期処理が可能になるのか?
そんな疑問に答える第 5 章は 「非同期処理の基本 (非同期処理 + Callback 関数)」 です。
(*この記事は、JavaScript について勉強した内容をまとめたものであり、内容が不正確な可能性があります。もし指摘などあれば、コメントいただけるととても嬉しいです。)