Svelte を使ったビューリファレンスシンタックスシュガー

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

目次

  1. 前言

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

前言

10月28日に尤雨溪氏が提案した RFC では、ref の宣言において JavaScript の label statement を用いてさらに簡素化できることが示されています。この構文は Svelte と非常に似ているので、自分の考えを記録しておきたいと思います。

まずはサンプルのコードを見てみましょう:

<script setup>
// label statement 構文を使って ref を宣言 
ref: count = 1

function inc() {
  // 変数を直接使用可能
  count++
}

// または、$ を接頭辞として ref を取得
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

Vue 3 では Composition API を通じて、ref を使って変数をリアクティブにすることができます。宣言方法は以下のようになります:(コードはドキュメントの例です)

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

ref を宣言した後はリアクティブな効果が生まれるため、template 内で使用すると画面も即時に更新されます:(コードはドキュメントの例です)

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      return {
        count: ref(0)
      }
    }
  }
</script>

ここで注意が必要なのは、ref を宣言した後は値を取得する際に .value を付けなければならないことです。これは大きな問題ではありませんが、いくつかのオーバーヘッドを引き起こします。 ドキュメント でも触れられている点は以下の通りです:

  • ユーザーは今使っているのが通常の変数なのか、ref で包まれた変数なのかを理解する必要がある(ただし、これは命名規則や TypeScript で解決可能です)。
  • 値を取得するたびに .value を別途付ける必要がある。

そこで著者は Ref Sugar という提案に至りました:

<script setup>
// label statement 構文を使って ref を宣言 
ref: count = 1

function inc() {
  // 変数を直接使用可能
  count++
}

// または、$ を接頭辞として ref を取得
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

上記のコードが変換されると次のようになります:

<script setup>
  const count = ref(1);
  function inc() {
    count.value++;
  }

  console.log(count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

この構文糖は ref の宣言自体を隠し、変数に直接アクセスできるようにし、.value を使わなくても済むようにすることで、開発者の心的負担を軽減します。

現在、コミュニティの大半は反対意見を持っており、大きく分けて以下の点が挙げられます:

  • これは合法な JavaScript 構文ではない。
  • Vue は label statement の意味を再定義している。
  • let や const を使って変数を宣言していない。
  • 追加のコンパイルが必要で、魔法のようなものが増えてしまう。
  • ...

興味があれば RFC を見てみてください。さまざまな面白い議論が行われています。

著者自身も、この考えは Svelte にインスパイアを受けたと言っています。それでは Svelte がこの構文をどのように使用しているか見てみましょう:

<script>
	let name = 'world';
	// name が更新されるたびに console.log(name) を再実行
	$: console.log(name);
  // name が更新されるたびに name2 に値を割り当て
	$: name2 = name.toUpperCase();
  // ???
	$: console.log('');
	
</script>

$ を付けた全てのコードは、Svelte によって次のように別途コンパイルされます:

...
let name2;
$$self.$$.update = () => {
  if ($$self.$$.dirty & /*name*/ 1) {
    $: console.log(name);
  }

  if ($$self.$$.dirty & /*name*/ 1) {
    $: name2 = name.toUpperCase();
  }
};

$: console.log("");

この構文については今は置いておいて、Svelte はラベルの後に宣言されたコードをコンパイル後に update という関数に配置します。コンポーネントが更新されると、この関数が実行され、Svelte はその中で変数が更新されているかをチェックして、コードを実行するかどうかを決定します。

また、最下部の console.log("") にも注目してください。$: のコードスニペット内でコンポーネント内の変数を使用していない場合、そのコードは update 関数に含まれないため、これは Svelte のコンパイル時の最適化の一つです。

2つ以上の変数がある場合、Svelte はどの変数が変化したかを把握し、対応するコードを実行します:

let name = 'world';
let count = 0;

// name が変化したときのみ実行
$: console.log(name);
// count が変化したときのみ実行
$: console.log(count);

詳細な原理については他の記事で紹介します

ここからわかることは、Svelte はできるだけ依存関係を追跡し、React の構文に変換すると次のようになります:

const [name, ] = useState('world');
const [count, ] = useState(0);

useEffect(() => {
  console.log(name);
}, [name]);

useEffect(() => {
  console.log(count)
}, [count])

つまり、Svelte はコンパイルを通じて内部の依存関係を事前に把握し、処理を行います。そのため、ランタイム時に依存関係を明示的に宣言する必要がありません。この部分には良い点と悪い点があり、一連の選択が含まれますが、今後詳しく見ていくことにしましょう。

ここでわかるのは、構文とコンパイルの助けを借りることでコードが大幅に簡素化でき、簡潔なコードは開発者の心的負担を軽減できるということです。

そのため、これは魔法のように見えることもあります。議論を集中させるためには、まず「魔法」の定義を明確にする必要があります。ここで私は魔法を以下のように定義します:

  • コードの挙動が予期しないものである(例えば、label の意味が書き換えられる)。
  • コードと実際のコンパイル後のコードが多くの手順を経る(label のコードがコンパイル後に全く異なる)。

これらの二点に基づく議論がさまざまな論争を引き起こすことができます:

  • JSX、template、SFC が多くの手順を経て最終的に JavaScript コードになるのは、魔法と見なされるべきか? なぜ開発者はこの構文を一般的に受け入れているのか?
  • v-ifv-showv-for などフレームワークが提供するテンプレート構文は魔法と見なされるべきか?
  • React の onChange イベントとネイティブブラウザの onChange が異なるのは、標準を再定義したことになるのか?(参考

魔法という観点から見ると、各フレームワークには多かれ少なかれ魔法の要素が含まれており、魔法という観点からの論点は少し薄弱に思えます。

しかし、Vue Ref Sugar の観点から見ると、この構文糖がもたらす利点は実際にはあまり多くありません。Svelte と異なり、Svelte の $: は内部の依存関係が変更されたときにコードを再実行する意味を持ちますが、Vue の ref: は単に ref 変数を宣言するだけです。構文は似ていますが、機能は全く異なります。

さらに、Svelte ではコンポーネント内の <script> にある変数宣言がデフォルトでリアクティブであるのに対し、Svelte はコンポーネント外で宣言するための類似のメカニズムを提供していません。つまり、Svelte では以下のようにすることはできません:

// Component.svelte
<script>
  // デフォルトでリアクティブ
  let state1 = 0;
  // デフォルトでリアクティブ
  let state2 = 1;
  
	function doSomething() {
    let state3 = 0; // リアクティブではない
  }
</script>

<span>{state1}</span>
<span>{state2}</span>

しかし、state1state2 を外部に渡すことができません:

// フェイク、こんな API はない
export const useStates = () => {
  const state1 = makeReactive(0);
  const state2 = makeReactive(1);
  
  return [state1, state2];
};
// Svelte ではこのようには書けない
<script>
  import { useStates } from './useStates';
	const [state1, state2] = useStates();
</script>

<span>{state1}</span>
<span>{state2}</span>

同様の効果を得るには、Svelte のストアを使用して宣言する必要がありますが、その範囲には違いがあります。ストアはグローバルであり、一度サブスクライブされると他のコンポーネントと共有されますが、Composition API はコンポーネント外でも機能し、単独で関数として宣言することができます。

すべての変数は Svelte コンポーネント内に置かなければならないため、変数が多くなると分割が難しくなり、メンテナンスがより困難になることがあります。特に習慣が悪い場合、変数があちこちに宣言されて管理が難しくなることがあります。今後、Svelte も外部にリアクティブな変数を宣言できるようなメカニズムを持つことを期待しています。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee

目次

  1. 前言