🧊

Svelteランタイムのコードを読む Part.1

コンパイラのコードを一通り読んだところなので、ランタイムもついでに読んでおこうかと。

はじめに

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()`

コンポーネントインスタンスに対しての初期化処理の実態。

やってることは、

`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[];
}

わかりやすくコンポーネントインスタンスっぽい。

  • `fragment`
    • `create_fragment()`の返り値
  • `ctx`
    • そのコンポーネント特有の`instance()`があればその返り値
    • 自身が管理するリアクティブな値とそのアップデート処理があれば、配列にその値が並ぶ
  • `context`
  • `dirty`
    • ビットマスクになってて、初期値は`[-1]` つまり汚れてる状態

`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`であり、それを`set()`と`get()`するだけ。

  • `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/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/*`も読んでしまいたい。