前言
在 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
將變數變成具有 reactive 的效果。宣告方式像這樣:(程式碼為文件範例)
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
經過 ref
宣告之後會有 reactive 的效果,所以在 template 當中使用的話畫面也會即時做更新:(程式碼為文件範例)
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
return {
count: ref(0)
}
}
}
</script>
在這邊有一個地方要注意的是,每次宣告 ref
之後要存取值需要加上 .value
,雖然不是什麼大問題,但還是引入了一些 overhead,在文件當中也有提到:
- 使用者需要知道他現在使用的是一般的變數,還是被
ref
包起來的變數(不過這可以透過 naming convention 或是 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 來宣告變數
- 需要經過一層編譯,等於多了一個 magic
- ...
有興趣的話可以到 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 在編譯時做得優化之一。
如果有兩個變數以上時,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 可以透過編譯的方式事先知道內部有哪些依賴,並且幫你做處理,這樣一來就不需要在 runtime 時顯式地去宣告依賴。當然這部分有好有壞,也包含一連串的取捨,我們以後再來細提。
從這裡可以發現,透過語法與編譯的幫助,可以大幅簡化程式碼,簡潔的程式碼可以降低開發人員的心智負擔。
也因為如此很容易被人當成是變魔術。為了讓討論更加集中,需要先釐清變魔術的定義是什麼,因此我在這邊將魔術定義為:
- 程式碼行為不符合預期(例如改寫 label 的語意)
- 程式碼與實際編譯後的程式碼經過太多步驟處理(label 的程式碼編譯後完全不一樣)
基於這兩者的討論就可以衍伸出各式各樣的辯論了:
- JSX、template、SFC 經過許多步驟處理最後變成 JavaScript 程式碼,算不算是變魔術?為什麼開發者普遍接受這樣的語法?
v-if
v-show
v-for
等框架提供的樣板語法算不算是變魔術?- React 的
onChange
事件和原生瀏覽器的onChange
不同,算不算重新定義了標準?(參考)
所以單純從變魔術的角度來看,每個框架本身或多或少都存在著魔術,單就變魔術這個角度來看論點似乎有點薄弱。
不過單從 Vue Ref Sugar 來看這個語法糖能夠帶來的好處其實蠻有限,他作用的效果與 Svelte 不同,Svelte 的 $:
的語意是當內部的依賴有改變時重新執行程式碼,而 Vue 的 ref:
則是單純宣告一個 ref
變數而已,雖然語法相近但功能上完全不同。
另外雖然在 Svelte 當中,任何在元件裡 <script>
的變數宣告都會是 reactive,但是 Svelte 並沒有提供類似的機制在元件外部宣告。也就是說雖然 svelte 可以做到這樣:
// Component.svelte
<script>
// reactive by default
let state1 = 0;
// reactive by default
let state2 = 1;
function doSomething() {
let state3 = 0; // not reactive
}
</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 當中的 store 來做宣告,不過在範圍上就有差異了。Store 本身是全域的,一旦被 subscribe 之後就會被其他元件共享,但是 composition API 本身可以作用在元件外部,單獨宣告成一個函數使用。
因為任何的變數都要放在 Svelte 元件中,所以一旦變數一多就很難做拆分,在維護上也會比較不容易一些,尤其是習慣不好的話變數到處亂宣告反而會變得不好管理。希望 Svelte 以後也有類似的機制可以在外部宣告 reactive 變數來簡化元件內的狀態。