カランのブログ

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

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

前書き

10/28に、尤雨溪氏が RFC を提案しました。この提案は、ref宣言の構文において、JavaScriptのlabelステートメントを使用してさらに簡略化するものです。 この構文は、Svelteと非常に似ており、ここでは自分の考えを記録しておきます。

まず、例として次のコードを見てみましょう:

<script setup>
// 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>
  <div>{{ count }}</div>
</template>

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

ここで注意すべき点は、refを宣言した後、値にアクセスするためには.valueを追加する必要があることです。これは大きな問題ではありませんが、いくつかのオーバーヘッドを導入していることに注意してください。ドキュメントでも触れられています:

  • ユーザーは、通常の変数を使用しているのか、refでラップされた変数を使用しているのかを認識する必要があります(ただし、ネーミング規則やTypeScriptを使用することで解決できます)。
  • 値を読み取るたびに.valueを追加する必要があります。

そこで、作者は Ref Sugar という提案をしました:

<script setup>
// 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ステートメントの意味を再定義しています。
  • 変数の宣言にletconstが使用されていません。
  • 1つのマジックが追加されるため、コンパイルの過程が必要です。
  • ...

興味があれば、RFCを参照して、いくつかの興味深い議論を見ることができます。

作者自身も、このアイデアはSvelteからの着想を借りていると述べています。では、Svelteがこの構文をどのように使用しているのか見てみましょう:

<script>
	let name = 'world';
	// nameが更新されるたびにconsole.log(name)が再実行されます
	$: console.log(name);
  // 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);

詳細な仕組みは別の記事で紹介します

ここからわかるように、構文とコンパイルの助けを借りることで、コードを大幅に簡略化することができます。シンプルなコードは、開発者の負担を軽減することができますが、同時にマジックとして認識されることもあります。議論をより集中させるために、まず「マジック」の定義を明確にする必要があります。ここで、「マジック」を次のように定義します:

  • コードの挙動が予想と異なる(例:labelの意味を変更する)
  • コードと実際にコンパイルされたコードが多くのステップで処理される(labelのコードは完全に異なる形式でコンパイルされる)

これらの2つの観点に基づいて議論を展開すると、さまざまな議論が生まれます:

  • JSX、テンプレート、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でも外部で反応的な変数を宣言するための類似のメカニズムが提供されることを望みます。

次の記事

偉大な神の奇妙な現象

前の記事

SSR のシナリオの考え方と使用方法

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee