· 7分で読了

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.textContentthis is textを代入する
  • mount
    • if_blocktrueであれば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_fragmentinstanceSvelteComponentという3つの主要部分で構成されている。

  • create_fragment:コンポーネントの各ライフサイクルをどのように処理すべきかをSvelteに伝える

  • instance<script>内のコードを実行し、context(propsや変数など)を返す

  • SvelteComponentinit関数を通じてSvelteコンポーネントを初期化する

この記事では、Svelteが生成したコードをどのように読み解くかを解説し、その背後にあるリアクティブの仕組みがどのように実現されているかを簡単に説明した(実際にはまだ言及していない仕組みもたくさんあるため、今後の記事で引き続き説明していく)。これでみんなもSvelteが生成したコードが読めるようになったはずだ!

もっとSvelteに関する記事を読みたい場合はここを参考にしてほしい。

関連記事

他のトピックを探索