前書き
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ステートメントの意味を再定義しています。
- 変数の宣言に
let
やconst
が使用されていません。 - 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-if
、v-show
、v-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>
しかし、state1
とstate2
を外部に分離することはできません:
// ダミー、このような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でも外部で反応的な変数を宣言するための類似のメカニズムが提供されることを望みます。