EY-Office ブログ

Svelteに入門2(Svelteの生成したコードを読んでみた)

Svelteに入門してみた(React/Vueとの違い) 続きです。

前回、「Svelteではコンパイル(Svelteのコードからブラウザーで実行できるJavaScriptに変換する処理)の際にデータ等の変更に対応してDOMを書き換えるコードを自動生成する事で宣言的UIを実現しています。」と書きましたが、今回はSvelteの生成した(コンパイルした)コードを読んで、その仕組みを調べてみました。

svelte

サンプルコード(App.svelte)

今回はSvelteの生成するコードを調べやすいような簡単なコードを書いてみました。画面は以下のようになります。

アプリの画面イメージ

コードの説明はあまり要らないとは思いますが、CountAボタンを押すとcounterA変数の値が増え、A:の表示が増えます。Bも同様です。ResetボタンはcounterA,counterBが0になります。

$: から始まる行はReactive statementsと呼ばれ、コンポーネントが再表示される際に実行されまる文です。$: を付けないとtotalの値はcounterAの増加では更新されません。

<script lang="ts">
  let counterA = 0
  let counterB = 0

  $: total = counterA + counterB

  function countA() {
    counterA++
  }
  function countB() {
    counterB++
  }
  function reset() {
    counterA = counterB = 0
  }
</script>

<main>
  <h1>Sample</h1>
  <button on:click={countA}>Count A</button>
  <button on:click={countB}>Count B</button>
  <button on:click={reset}>Reset</button>
  <div>A: {counterA}</div>
  <div>B: {counterB}</div>
  <div>Total: {total}</div>
</main>

生成されたコード

Svelteの開発環境ではコンパイルされたコードはpublic/build/bundle.jsに出力されています。開発モードではデバッグ様にコードが追加されているので、今回はプロダクションモード(ただしminify無し)のコードで説明します。

bundle.jsファイルの先頭の約250行は実行時に必要な関数やクラスです。

Appクラス

サンプルコードApp.svelteはコンパイルされると以下のようなAppクラスになります。その下がメインプログラムmain.tsファイルのコンパイルされたものです。

AppクラスはSvelteComponentを継承したクラスです、SvelteComponentクラスは非常にコンパクトなクラスでプロダクションモードではあまり機能はないので解説は省略します。

コンストラクターから呼び出されるinit関数でコンポーネントの構築と初期化が行われています。init関数に渡されているinstanceが<script>に対応し、create_fragmentがHTML部分に対応しています。

class App extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, instance, create_fragment, safe_not_equal, {});
  }
}

const app = new App({
    target: document.body,
    props: {}
});

return app;

instance関数

<script lang="ts">に書かれている変数、関数です。init関数からinstanceを呼び出すさいに渡されている$$invalidat関数の定義も下に書いておきました。

変数の宣言はソースコードと同じですが、countA等の関数は$$invalidat関数を使っています。この関数の第1引数は状態を管理しているctx配列の添字で0がcounterA、 1がcounterB、2がtotalの状態(現状の値)を保持しています。第2引数が更新する式、更新する式の戻り値が定かではない場合は第3引数に新規の値を渡しています。

function instance($$self, $$props, $$invalidate) {
  let total;
  let counterA = 0;
  let counterB = 0;

  function countA() {
    $$invalidate(0, counterA++, counterA);
  }

  function countB() {
    $$invalidate(1, counterB++, counterB);
  }

  function reset() {
    $$invalidate(0, counterA = $$invalidate(1, counterB = 0));
  }

  $$self.$$.update = () => {
    if ($$self.$$.dirty & /*counterA, counterB*/ 3) {
      $$invalidate(2, total = counterA + counterB);
    }
  };

  return [counterA, counterB, total, countA, countB, reset];
}

$$invalidat関数では状態を変更する式が実行された後で状態が更新されたかチェックし、変更された場合はmake_dirty関数を使い$$.dirtyの対応するbitをオンにします。

const $$invalidate = (i, ret, ...rest) => {
    const value = rest.length ? rest[0] : ret;
    if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
        if (!$$.skip_bound && $$.bound[i])
            $$.bound[i](value);
        if (ready)
            make_dirty(component, i);
    }
    return ret;
}

create_fragment関数

create_fragment関数はDOMを構築する関数群を戻す関数です。

  • c関数で使われているelementはdocument.createElementでHTML要素を作成し変数に代入しています
  • m関数は作成されたHTML要素の配置です、insert、appendはDOM操作APIのinsertBefore、appendChild関数です。最後の方にあるlistenはaddEventListenerでボタンにイベント・ハンドラーを設定しています
  • p関数はイベント処理等の後で呼び出される関数です
    • dirtyは変数(状態)に変更されたかどうかの情報で、各bitが対応する変数が更新されたかを表しています
    • dirtyのbitを調べ、変数が更新された場合は必要なDOMのみset_data関数で変更しています
function create_fragment(ctx) {
  let main;
  let h1;
  let t1;
  let button0;
  let t3;
  let button1;
  let t5;
  let button2;
  let t7;
  let div0;
  let t8;
  let t9;
  let t10;
  let div1;
  let t11;
  let t12;
  let t13;
  let div2;
  let t14;
  let t15;
  let mounted;
  let dispose;

  return {
    c() {
      main = element("main");
      h1 = element("h1");
      h1.textContent = "Sample";
      t1 = space();
      button0 = element("button");
      button0.textContent = "Count A";
      t3 = space();
      button1 = element("button");
      button1.textContent = "Count B";
      t5 = space();
      button2 = element("button");
      button2.textContent = "Reset";
      t7 = space();
      div0 = element("div");
      t8 = text("A: ");
      t9 = text(/*counterA*/ ctx[0]);
      t10 = space();
      div1 = element("div");
      t11 = text("B: ");
      t12 = text(/*counterB*/ ctx[1]);
      t13 = space();
      div2 = element("div");
      t14 = text("Total: ");
      t15 = text(/*total*/ ctx[2]);
    },
    m(target, anchor) {
      insert(target, main, anchor);
      append(main, h1);
      append(main, t1);
      append(main, button0);
      append(main, t3);
      append(main, button1);
      append(main, t5);
      append(main, button2);
      append(main, t7);
      append(main, div0);
      append(div0, t8);
      append(div0, t9);
      append(main, t10);
      append(main, div1);
      append(div1, t11);
      append(div1, t12);
      append(main, t13);
      append(main, div2);
      append(div2, t14);
      append(div2, t15);

      if (!mounted) {
        dispose = [
          listen(button0, "click", /*countA*/ ctx[3]),
          listen(button1, "click", /*countB*/ ctx[4]),
          listen(button2, "click", /*reset*/ ctx[5])
        ];

        mounted = true;
      }
    },
    p(ctx, [dirty]) {
      if (dirty & /*counterA*/ 1) set_data(t9, /*counterA*/ ctx[0]);
      if (dirty & /*counterB*/ 2) set_data(t12, /*counterB*/ ctx[1]);
      if (dirty & /*total*/ 4) set_data(t15, /*total*/ ctx[2]);
    },
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(main);
      mounted = false;
      run_all(dispose);
    }
  };
}

まとめ

instance関数の中で変数(状態)の変化を記録し、イベント処理等の後の更新処理でcreate_fragment関数内のp関数で変更された変数(状態)に対応するDOMのみ変更が行われるようになっています。

これらを繋ぐコードも大きくないので読むのは難しくありませんが、今回はこれくらいで。
{#if ...}などの実装にも興味はあるので、続きもあるかも・・・

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ