コンパイラのコードを一通り読んだところなので、ランタイムもついでに読んでおこうかと。
はじめに
Svelteのランタイムのコードは、おおきく2種類ある。
前者が1つだけで、あとはすべて後者だったりする。
ちなみにランタイム = クライアントサイドの話 = コンパイラを`{ generate: "dom" }`して使う場合の話。
ネームスペースとAPI
- `svelte`
- `svelte/store`
- `svelte/motion`
- `svelte/transition`
- `svelte/easing`
- `svelte/animate`
- `svelte/register`
現状ではこれだけのモジュールが`export`されてる。
さて、これらの使い方・・ではなく、中でどういうことやってるのかを順に見ていく。
from `svelte`
ランタイムのベースとなるコードたちで、公開されてるものが以下のとおり。
- `onMount()`
- `onDestroy()`
- `beforeUpdate()`
- `afterUpdate()`
- `setContext()`
- `getContext()`
- `tick()`
- `createEventDispatcher()`
- `SvelteComponent`
いわゆるコンポーネントのライフサイクルのフックに処理を追加する関数たちと、コンポーネントそれ自身のコード。
表向きにモジュールとしてエクスポートされてるのがコレだけってだけで、実際にランタイムで走るコードという意味ではもっといろいろあって、`svelte/internal`ってネームスペースにある。
`SvelteComponent`
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/Component.ts
ちなみにこのファイルでは、`CustomElement`のための`SvelteElement`の実装とかも入ってる。
さて、`*.svelte`ファイルは、コンパイラによってこの`SvelteComponent`に集約されてランタイムになる。
コンパイル時に`{ dev: true }`が指定されてた場合は、かわりに`SvelteDevComponent`ってのが使われる。
基本的には同じだが、デバッグ用のコードやらイベントやらが残ったままになる。
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dev.ts
この`SvelteComponent`は直接使うのではなく、各コンポーネントがそれを`extends`して使うようになっており、だいたいこんな感じになる。
// App.svelteの場合 class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, create_fragment, safe_not_equal, {}); } }
というわけで、コンパイラ編で見かけた`init()`と`create_fragment()`についにご対面というわけ。
`init()`
やってることは、
- コンポーネントの核ともいえる`$$`プロパティを仕込む
- `beforeUpdate()`の実行
- `create_fragment()`
- `intro: true`なら、トランジションのINを実行
- `mount_component()`
- `flush()`
`component.$$`
コンポーネントのすべてといっても過言ではないプロパティ。
interface T$$ { fragment: null | false | Fragment; ctx: null | any; // state props: Record<string, 0 | string>; update: () => void; not_equal: any; bound: any; // lifecycle on_mount: any[]; on_destroy: any[]; before_update: any[]; after_update: any[]; context: Map<any, any>; callbacks: any; dirty: number[]; }
`create_fragment()`
処理自体はランタイムのコードの中にはなくて、コンパイラが生成するもの。
型としてはこのように。
interface Fragment { key: string | null; first: null; /* create */ c: () => void; /* claim */ l: (nodes: any) => void; /* hydrate */ h: () => void; /* mount */ m: (target: HTMLElement, anchor: any) => void; /* update */ p: (ctx: any, dirty: any) => void; /* measure */ r: () => void; /* fix */ f: () => void; /* animate */ a: () => void; /* intro */ i: (local: any) => void; /* outro */ o: (local: any) => void; /* destroy */ d: (detaching: 0 | 1) => void; }
だいたいのやってることはこんな感じ。
- `c`: create
- DOMの生成(`createElement()`とか)
- `m`: mount
- DOMの挿入(`insertNode()`とか`appendChild()`とか)
- `p`: update
- DOMの更新(`setAttribute()`とか`input.value =`とか`textNode.data = `とか)
- `d`: destroy
- DOMから削除(`removeChild()`とか)
このへんのコードはココにあるものが使われてる。
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts
プロパティ名はミニファイできないから、元から短く命名するという涙ぐましい努力を感じる。
`mount_component()` / `flush()`
そのコンポーネントの`fragment.m()`を実行する。
その際に、`onMount()`のライフサイクルでやることがあれば実行して、その返り値が関数なら、`onDestory()`で実行されるように登録してる。
Issueでよく`onMount()`で非同期のコードを書きたい話が出てくるけど、安易に`async`を渡して返り値を`Promise`にしてしまうと、`onDestory()`に処理してもらえなくなるのはこのせい。
レンダリング後の各種コールバック(ライフサイクル含む)は、それ用の配列に貯められて、非同期にMicrotaskで実行される。
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/scheduler.ts
このファイルにある配列がそれで、`tick()`や`flush()`がそれらを実際に実行する処理。
それ以外
`SvelteComponent`以外のものたち。
- `onMount()`
- `onDestroy()`
- `beforeUpdate()`
- `afterUpdate()`
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/lifecycle.ts
基本的に、コンポーネントの`$$`にあるコールバック用の配列に関数を登録するだけで、あとはコンポーネントが任意のタイミングで呼び出す。
このファイルには他にも、`set_current_component()`と`get_current_component()`というプライベートな関数があって、グローバルにどのコンポーネントの処理をするかを切り替えてる。
- `setContext()`
- `getContext()`
コンポーネントの`$$.context`が`Map
- `tick()`
先のレンダリング用の配列をいったん空にするまで`flush()`してる。
- `createEventDispatcher()`
自身のコンポーネントに対して、任意のイベントを発行できる`dispatch()`を得るためのもの。
任意の`type`でコールバックを登録できるようにするための仕組み。
`svelte/internal`の落ち穂拾い
. ├── Component.ts ├── animations.ts ├── await_block.ts ├── dev.ts ├── dom.ts ├── environment.ts ├── globals.ts ├── index.ts ├── keyed_each.ts ├── lifecycle.ts ├── loop.ts ├── scheduler.ts ├── spread.ts ├── ssr.ts ├── style_manager.ts ├── transitions.ts └── utils.ts
ファイルとしてはこれだけあるので、気になるものを見ておく。
テンプレート補助
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/await_block.ts
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/keyed_each.ts
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/spread.ts
`markup`部で使える記法のためのヘルパーたち。
アニメーション・トランジション
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/animations.ts
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/transitions.ts
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/style_manager.ts
エフェクトを実行するためのベースの処理たちで、かなり泥臭い処理になってる・・。
トランジションは、内部的な処理でもDOMの`CustomEvent`を使ってる。
- `introstart`
- `introend`
- `outrostart`
- `outroend`
なのでこれらのイベントが拾える。
`internal/loop`
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/loop.ts
コンポーネントの各種レンダリングは、`Promise.resolve()`をチェーンしてMicrotaskで実行される。
こっちはさっきのアニメーション用で、`requestAnimationFrame()`が使われてる。
`internal/ssr`
https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/ssr.ts
SSR用のコンポーネント(文字列と関数)を返す用の処理がまとまってる。
まとめ
- コンパイラが生成したコードは、ランタイムで読み込めるAPIを使って動く
- `svelte`コアから`import`できるもの
- コンポーネントの実装そのもの
- 各種ライフサイクルへの関数を登録するフック
次の記事で残りの`svelte/*`も読んでしまいたい。