カランのブログ

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

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はオブジェクトを返し、それぞれのアルファベットが関数の属性として使われているため、何をしているのかわからないように見えますが、実際には異なるライフサイクルで実行されるアクションを表しています。

  • c: createを表し、コンポーネントが作成された直後に実行される処理です。
  • m: mountを表し、コンポーネントがDOMにマウントされた後に実行される処理です。
  • p: patchを表し、コンポーネントが更新された後に実行される処理です。
  • i: introを表し、コンポーネントがトランジションで入場する際に実行される処理です。
  • o: outroを表し、コンポーネントがトランジションで退場する際に実行される処理です。
  • d: destroyまたはdetachを表し、コンポーネントがアンマウントされる際に実行される処理です。

各単語が何を意味するかを知っている場合、それらが行っていることはより明確になります。

  • 条件式(if count != 100)の結果をif_blockに代入します。
  • create時には、
    • p要素を作成します。
    • p.textContentthis is textに設定します。
  • mount時には、
    • if_blocktrueの場合、if_block.m()(つまり、マウント時に実行する処理)を呼び出します。
    • t0をアンカーに挿入します。
    • pをアンカーに挿入します。
  • patch時には、
    • 条件式count != 100trueの場合、
      • if_blockが既に存在する場合はif_block.p()を呼び出します。
      • そうでない場合は、create_if_blockを一度呼び出してからif_block.m()を実行します。
    • 条件式count != 100falseの場合、
      • 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のようなものです。違いは、一方は手動で追加する必要があり、もう一方は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が呼び出され、まず更新前と後の値を比較し、更新がある場合はmark_dirty関数を呼び出してcomponentを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に関する記事を読みたい場合は、こちらを参照してください。

次の記事

2022年にスヴェルトを学ぶべき4つの理由

前の記事

このモノの価値

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee