前言
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.textContent
をthis is text
に設定します。
mount
時には、if_block
がtrue
の場合、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
のようなものです。違いは、一方は手動で追加する必要があり、もう一方は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_fragment
、instance
、SvelteComponent
で構成されています。
create_fragment
:Svelteコンポーネントの各ライフサイクルで実行するアクションを指定します。instance
:<script>
内のコードを実行し、コンテキスト(props、変数など)を返します。SvelteComponent
:init
関数を呼び出してSvelteコンポーネントを初期化します。
この記事では、Svelteが生成するコードを解析する方法と、その背後にあるリアクティブな仕組みがどのように実現されているか(実際にはまだ説明していない多くの仕組みがありますが、後の記事で説明します)を説明しました。これで、誰でもSvelteが生成するコードを理解できるようになりました!
さらにSvelteに関する記事を読みたい場合は、こちらを参照してください。