カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

前書き

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

作者

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

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください