Svelteを深く理解する(2)— Svelteが生成するコードの分析
はじめに
Svelteのコアコンセプトからわかるように、Svelteはコンパイルプロセスから必要な情報を可能な限り取得し、動的なオーバーヘッドを削減することを目指している。前回の記事ではSvelteがコンパイルからコード生成までどのように動作するのかを説明したが、今回はSvelteが生成したコードがどのように動作しているのかを観察してみよう。
まずはシンプルなSvelteコンポーネントを見てみよう:
<script>
import { onMount } from 'svelte';
let count = 1;
onMount(() => {
setInterval(() => count++, 1000);
})
</script>
{#if count != 100}
<span>{count}</span>
{/if}
<p>
this is text
</p>
Svelteコンポーネントの構文は一般的なHTMLと同じであり、テンプレート構文のようなもの(if, awaitなど)が加わる以外は基本的にHTMLと完全な互換性がある。しかし、生成されるコンポーネントはJavaScriptだ。例えば、上記のコンポーネントをコンパイルすると次のようになる:
// 由 Svelte 生成,省略部分程式碼
import { onMount } from "svelte";
function create_if_block(ctx) {
let span;
let t;
return {
c() {
span = element("span");
t = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, span, anchor);
append(span, t);
},
p(ctx, dirty) {
if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
d(detaching) {
if (detaching) detach(span);
}
};
}
function create_fragment(ctx) {
let t0;
let p;
let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);
return {
c() {
if (if_block) if_block.c();
t0 = space();
p = element("p");
p.textContent = "this is text";
},
m(target, anchor) {
if (if_block) if_block.m(target, anchor);
insert(target, t0, anchor);
insert(target, p, anchor);
},
p(ctx, [dirty]) {
if (/*count*/ ctx[0] != 100) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
if_block = create_if_block(ctx);
if_block.c();
if_block.m(t0.parentNode, t0);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
},
i: noop,
o: noop,
d(detaching) {
if (if_block) if_block.d(detaching);
if (detaching) detach(t0);
if (detaching) detach(p);
}
};
}
function instance($$self, $$props, $$invalidate) {
let count = 1;
onMount(() => {
setInterval(() => $$invalidate(0, count++, count), 1000);
});
return [count];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
Svelte自体もSSR機能をサポートしているため、SvelteのSSR機能を使って上記のコードをコンパイルすると、HTML文字列を生成する関数が作成される。
// 由 Svelte 生成,省略部分程式碼
import { onMount } from "svelte";
const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
let count = 1;
onMount(() => {
setInterval(() => count++, 1000);
});
return `${count != 100 ? `<span>${escape(count)}</span>` : ``}
<p>this is text
</p>`;
});
export default App;
生成されたコード(dom)を観察する
説明をわかりやすくするため、ここではdom用の生成コードのみを解説し、SSRの部分はいったんスキップする。
生成されたコードは主に3つの部分から構成されていることがわかる:create_fragment関数、instance関数、そしてSvelteComponentクラスだ。
create_fragment
まずはcreate_fragmentを見てみよう:
function create_fragment(ctx) {
let t0;
let p;
let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);
return {
c() {
if (if_block) if_block.c();
t0 = space();
p = element("p");
p.textContent = "this is text";
},
m(target, anchor) {
if (if_block) if_block.m(target, anchor);
insert(target, t0, anchor);
insert(target, p, anchor);
},
p(ctx, [dirty]) {
if (/*count*/ ctx[0] != 100) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
if_block = create_if_block(ctx);
if_block.c();
if_block.m(t0.parentNode, t0);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
},
i: noop,
o: noop,
d(detaching) {
if (if_block) if_block.d(detaching);
if (detaching) detach(t0);
if (detaching) detach(p);
}
};
}
create_fragmentはオブジェクトを返すが、その中には1文字の英文字をプロパティ名にした関数がたくさん並んでおり、一見何をしているのかわからないかもしれない。だが、実際にはそれぞれ異なるライフサイクルで行うべき処理を表している:
- c:
createを表し、コンポーネントが作成された直後に実行される関数 - m:
mountを表し、コンポーネントがDOMにマウントされた後に実行される関数 - p:
patchを表し、コンポーネントが更新された後に実行される関数 - i:
introを表し、コンポーネントのトランジション(イン)時に実行される関数 - o:
outroを表し、コンポーネントのトランジション(アウト)時に実行される関数 - d:
destroyまたはdetachを表し、コンポーネントがアンマウントされる時に実行される関数
詳細なソースコードと生成ロジックは src/compiler/compile/render_dom/Block.ts を参照してほしい。
それぞれの文字が意味することを把握すれば、それらが行っている処理もかなり明確になる:
- 条件式(
if count != 100)の結果をif_blockに代入する create時p要素を作成するp.textContentにthis is textを代入する
mount時if_blockがtrueであればif_block.m()を呼び出す(つまりマウント時の処理を行う)t0をanchorに挿入するpをanchorに挿入する
patch時- 条件式
count != 100がtrueの場合- すでに
if_blockが存在していればif_block.p()を呼び出す - なければ
create_if_blockを一度呼び出してからif_block.m()を実行する
- すでに
- 条件式
count != 100がfalseの場合if_block内の要素を削除することを意味し、if_block.d(1)を呼び出す
- 条件式
instance
次にinstance関数を見てみよう:
function instance($$self, $$props, $$invalidate) {
let count = 1;
onMount(() => {
setInterval(() => $$invalidate(0, count++, count), 1000);
});
return [count];
}
<script>内のコードはすべてinstance関数の中に詰め込まれる。ここではいくつか特徴的な点がある:
- 元のコードは
setInterval(() => count++, 1000)だったが、生成後はsetInterval(() => $$invalidate(0, count++, count), 1000)になっている - 戻り値は配列であり、
countの値を返している
Svelteは静的解析の段階で変数に関する情報を把握するため、依存関係の追跡を行ってくれる。ここでの$$invalidateの動作は、ReactにおけるsetStateに少し似ている。ただ、前者は手動で記述する必要があるのに対し、後者はSvelteが自動的に検出して処理してくれるという違いがある。
$$invalidateの実装は次のようになっている(一部のコードを省略):
// 如果發現變數值不同,將 component 設為 dirty (代表需要更新)
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i);
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
setIntervalによってcount++がトリガーされるたびに$$invalidateが呼び出される。このとき、まず更新前後の値が同じかどうかを比較し、更新があればmake_dirty関数を呼び出してコンポーネントをdirty_componentsに追加し、更新をスケジュールする。Svelteもバッチ更新のような仕組みを実装しており、1フレーム内で可能な限りまとめて一度に更新しようとする。
SvelteComponent
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
SvelteComponentの実装は非常にシンプルで、init関数を呼び出しているだけだ。その中の主なロジックはSvelteコンポーネントを初期化し、create_fragment関数を呼び出してinstanceを実行し、コンポーネントを実際にDOMへマウントすることである。
まとめ
Svelteが生成するコードは、大まかにcreate_fragment、instance、SvelteComponentという3つの主要部分で構成されている。
-
create_fragment:コンポーネントの各ライフサイクルをどのように処理すべきかをSvelteに伝える -
instance:<script>内のコードを実行し、context(propsや変数など)を返す -
SvelteComponent:init関数を通じてSvelteコンポーネントを初期化する
この記事では、Svelteが生成したコードをどのように読み解くかを解説し、その背後にあるリアクティブの仕組みがどのように実現されているかを簡単に説明した(実際にはまだ言及していない仕組みもたくさんあるため、今後の記事で引き続き説明していく)。これでみんなもSvelteが生成したコードが読めるようになったはずだ!
もっとSvelteに関する記事を読みたい場合はここを参考にしてほしい。
関連記事
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。
- なぜウェブで Pixel Perfect を追求すべきではないのか Pixel Perfect はそれが本当に重要な場合にのみ意識すべきであり、そうでなければ往々にしてお互い損をする結果に陥ってしまう。