Svelteコンパイラのソースコードリーディング記事の続編の続編。
`svelte/compiler`の`compile()`を追っている途中。
前回までのあらすじ
- `compiler.parse()`によって得られるのがSvelteのAST
- それを使って`Component`クラスを初期化した
- ASTを元に、どのように振る舞うコンポーネントなのかがまとまった
render_dom() と render_ssr()
https://github.com/sveltejs/svelte/tree/master/src/compiler/compile/render_dom
https://github.com/sveltejs/svelte/tree/master/src/compiler/compile/render_ssr
DOMとSSRという2つのレンダリング方法があって、引数に`Component`のインスタンスを渡す。
どっちが使われるかは、`compile()`に渡された`generate`オプション次第。
どちらも返り値は、`js`と`css`という2つのプロパティがあるオブジェクト。
`css`は同じものが返ってくるみたい。
`js`のほうはDOMとSSRで結果が異なる。(具体的な違いは、前回の記事を参照)
というわけで繰り返しになるけど、ランタイムで動くコードをレンダリングするのが、この関数たちの仕事。
render_dom()
やってることの流れ。
- `Renderer`クラスの初期化
- (後述)
- `body`という配列の用意
- ここに、最終的な実行コードをASTで組み上げたものを積んでいく
- cssのコンパイル
- `Component`の`Stylesheet`から
- ここでScopedになったCSS文字列になる
- `Block`の適用
- `{#if}`とか`{#each}`とかで生まれる概念
- ブロックごとにコンテキスト(スコープ)が異なる
- ブロックは、いくつかのノードにまたがる
- 上層のコンポーネントから`props`として受け取るもののハンドリング
- 後に`$$props`に置き換え
- ASTの`instance`(`script`部)の精査
- `AssignmentExpression`か`UpdateExpression`があったら、`$$invalidate()`するよう置き換え
- `create_fragment()`の挿入
- ランタイムでも使われるやつ
- 各コンポーネントごとに用意される
- `context=module`部の挿入
- コンポーネントの実態である`instance()`の組み上げ
- ランタイムでも使われるやつ
- リアクティブな変数のアップデート時に再レンダリングなどするよう紐付け
- エクスポートするクラスのテンプレートの組み上げ
- CustomElementの場合、`SvelteElement`
- それ以外の場合、`SvelteComponent`
- `js`はAST、`css`は実行可能な文字列になったものを返す
理解するのも難しければ、説明するのも難しい!
`*.svelte`コンポーネントを`parse()`して得たASTを元に得られた`Component`クラスを使って、ランタイムで動くコードのASTを、JavaScriptで動的に作ることをやってる。
ASTをJavaScriptで作る
ASTをJavaScriptで作るとはどういうことかは、その処理のコアであるこのライブラリを見たほうがはやい。
GitHub - Rich-Harris/code-red: Experimental toolkit for writing x-to-JavaScript compilers
READMEから転載したコードはこんな感じ。
import { b, x } from "code-red"; const expression = x`i + j`; assert.equal(expression.type, "AssignmentExpression"); assert.equal(expression.operator, "+"); assert.equal(expression.left.name, "i"); assert.equal(expression.right.name, "j"); const body = b` const i = 1; const j = 2; const k = i + j; `; assert.equal(body.length, 3); assert.equal(body[0].type, "VariableDeclaration");
Tag付きのテンプレートリテラルで書いたコード片が、ESTreeのBodyになるという仕組み。
ASTを制するものはすべてを制す!という心意気を感じる・・・。
なんしかこれを使って、動的にランタイムのコードを組み上げていく。
- 参照だけされてる変数や純関数なら巻き上げてしまう
- 更新されてるならインスタンスのスコープに動かす
みたいな最適化(実際はもっとたくさんのことを、あらゆる条件でやってる)を、動的にやってるというわけ。
compiler/compile/render_dom/Renderer.ts
https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/render_dom/Renderer.ts
- `fragment`プロパティに、`Fragment`クラスを初期化
- ツリーと同時に、`Block`とインスタンスごとに必要なコンテキスト(後の`$$ctx`)も管理してる
- 状態の更新にあわせて、再描画などの処理が必要かどうかの判断 = `dirty`かどうかの判断もココでやってる
- チェックはビット演算
- 正確には、各ノードがその時の依存変数名のリストを渡して、それ用のビットマスクを用意してもらうイメージ
ツリーは`Fragment`で、そこに各種ラッパーに包まれたノードがぶら下がってる。
Svelte的な各ノードの要素の実態は`compiler/compile/nodes`にあって、`dom`用のラッパーと、`ssr`用のラッパーが別々に存在してるというわけ。
各ラッパは`Block`への参照を持ってて、処理の単位を協調させてるっぽい。
で、各ノード(ラッパー)がそれぞれ`render()`関数を持ってて、ランタイムで必要なときに呼ばれてアップデートされるという仕組み。
render_ssr()
- `dom`とは別の`Renderer`を使う
- ラッパの代わりにハンドラという層があって、各ノードを包んでる
- 各ノードはクラスではなく、文字列を返すようになってる
- `Renderer`の`add_string()`を呼んで、文字列を組み上げてる
- その時点でわからないものは、`add_expression()`
- `code-red`を使って、同様の流れでASTを組み上げるところは同じ
`render_dom()`に比べると、やはり圧倒的にコード量が少ない・・・!
component.generate()
`compile()`で行われる最後の処理。
`render_dom()`か`render_ssr()`で得られた`{ js, css }`な結果を引数に実行される。(正確には`null`が渡されることもあるけど)
ちなみにこの`Component.generate()`の返り値が、`compile()`自体の返り値になっており、ついに実行可能なJavaScriptの文字列が得られるところ。
やってることはこの通りで単純。
- バナーの挿入
- Svelteバージョンいくつで生成しました的な
- ASTを歩いて、`helpers`プロパティに関数を抽出(`@`ではじまる関数を探してる)
- これらがランタイムで呼ばれる関数名たち
- `create_module()`
- 引数は、AST本体、モジュールのフォーマット(`esm` or `cjs`)、`helpers`、バナー、などなど
- (後述)
- `css`の文字列化(既にそうなってるので代入だけ)
- `js`プロパティの文字列化
- `code-red`の`print()`を使ってる
ついに総仕上げで、モジュールとして書き出すところをやってる。
create_module()
https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/create_module.ts
- `esm`か`cjs`かどちらかのフォーマット
- `import`か`require()`かで微妙に完成するコードが変わる
- どちらも単一のモジュールを返すようになってる
ここは関数名からイメージできるそのまんまの処理だった。
まとめ
- `render_dom()`
- `component.generate()`
- ASTから実行可能な文字列に変換する処理
- モジュールとして使えるように
というわけでこれでコンパイラは一通り読めたことになる・・・はず・・。
ただコンパイル結果には、ランタイムで読み出す変数や関数がいっぱい含まれてる(`SvelteComponent`とか)ので、近いうちにそっちも読みたい。
コンパイラ総まとめ
Svelteのコンパイラのコードを読むシリーズは、ひとまずこれで完結。
- `preprocess()`
- Svelteの構文はそのままに、事前に処理が必要なら行う
- `parse()`
- SvelteのASTを生成する
- `compile()`
- ASTを解析し、環境ごとのASTを生成し、最終的に実行可能文字列へ
という3つの棲み分けはわかったし、Issueの読解も捗る予感がする。(コントリビュートできるかは別問題ではある)
コードを読んでみての感想は、
- 想像してた以上に複雑だった
- ASTに慣れてないとつらい
- というか、基本的にAST芸である
- コメントはそんなに書かれてないので、コード読むのそこそこ大変
- コード内に`TODO`も結構残ってた
まあ現世におけるモダンなフレームワークの中身を読むの、どれを取っても大変なんやろうし、やはり我々は生かされておるのじゃ・・。