Svelte の深い理解 (2) — Svelte 生成コードの分析

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

前言

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 はオブジェクトを返し、その中には多くの単一の英文字の関数が属性として含まれています。これらはそれぞれ異なるライフサイクルで実行されるべきことを表しています:

  • 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.textContentthis is text を代入
  • mount
    • if_blocktrue の場合、if_block.m() を呼び出す(これはマウント時に行うべきこと)
    • t0 をアンカーに挿入
    • p をアンカーに挿入
  • patch
    • 条件式 count != 100 が true の場合
      • すでに if_block があれば if_block.p() を呼び出す
      • ない場合は create_if_block を呼び出し、if_block.m() を実行
    • 条件式 count != 100 が false の場合
      • if_block 内の要素を削除する必要があるので if_block.m() を呼び出す

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 のようなものです。ただし、Reactは手動で追加する必要がありますが、Svelteは自動で検出して処理します。

$$invalidate の実装は次のようになります(部分的なコードを省略):

// 変数の値が異なることがわかった場合、コンポーネントを 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));
}

毎回 setIntervalcount++ をトリガーすると、$$invalidate が呼び出されます。この時、更新前後の値が同じかどうかを比較し、異なる場合は mark_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が生成するコードは大きく分けて3つの部分から成ります:create_fragmentinstanceSvelteComponent

  • create_fragment:Svelteコンポーネント内の各ライフサイクルがどのように処理されるべきかを示す。
  • instance<script> 内のコードを実行し、コンテキスト(props、変数など)を返す。
  • SvelteComponentinit 関数を使ってSvelteコンポーネントを初期化する。

この記事では、Svelteが生成するコードをどのように解析し、背後にあるリアクティブなメカニズムがどのように達成されるのかを簡単に説明しました(実際にはまだ多くのメカニズムが未提起ですが、今後の記事で説明します)。これで皆さんもSvelteが生成するコードを理解できるようになりましたね!

Svelteに関する他の記事をもっと見たい方はこちらを参照してください。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee