Vue ref 語法糖與 Svelte

前言
在 10/28 時尤雨溪提出了一個 RFC,是關於 ref 宣告的語法中,可以用 JavaScript 的 label statement 來進一步做簡化。 這個語法跟 Svelte 如出一徹,在這邊紀錄一下自己的想法。
先來看看範例當中的程式碼:
html
<script setup>// 透過 label statement 語法宣告 refref: count = 1function inc() {// 可以直接取用變數count++}// 或是直接使用 $ 當作前綴來拿 refconsole.log($count.value)</script><template><button @click="inc">{{ count }}</button></template>
在 Vue 3 當中透過 Composition API,可以透過 ref
將變數變成具有 reactive 的效果。宣告方式像這樣:(程式碼為文件範例)
js
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
經過 ref
宣告之後會有 reactive 的效果,所以在 template 當中使用的話畫面也會即時做更新:(程式碼為文件範例)
html
<template><div>{{ count }}</div></template><script>export default {setup() {return {count: ref(0)}}}</script>
在這邊有一個地方要注意的是,每次宣告 ref
之後要存取值需要加上 .value
,雖然不是什麼大問題,但還是引入了一些 overhead,在文件當中也有提到:
- 使用者需要知道他現在使用的是一般的變數,還是被
ref
包起來的變數(不過這可以透過 naming convention 或是 TypeScript 解決) - 每次讀取值需要另外
.value
於是作者有了 Ref Sugar 這個提案:
html
<script setup>// 透過 label statement 語法宣告 refref: count = 1function inc() {// 可以直接取用變數count++}// 或是直接使用 $ 當作前綴來拿 refconsole.log($count.value)</script><template><button @click="inc">{{ count }}</button></template>
上面的程式碼經過轉換後會變成:
html
<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 是怎麼使用這個語法的:
html
<script>let name = 'world';// 每次 name 有更新時重新執行 console.log(name)$: console.log(name);// 每次 name 有更新時賦值給 name2$: name2 = name.toUpperCase();// ???$: console.log('');</script>
任何以 $
當作標籤的程式碼,會被 Svelte 另外編譯成這個樣子:
js
...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 可以知道哪些變數有變化,並且執行對應的程式碼:
js
let name = 'world';let count = 0;// 只有 name 有變化時才會執行$: console.log(name);// 只有 count 有變化時才會執行$: console.log(count);
詳細原理會在其他文章作介紹
在這邊可以知道 Svelte 會盡可能地追蹤依賴,如果轉換成 React 的語法,大概會像這個樣子:
js
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 可以做到這樣:
html
// Component.svelte<script>// reactive by defaultlet state1 = 0;// reactive by defaultlet state2 = 1;function doSomething() {let state3 = 0; // not reactive}</script><span>{state1}</span><span>{state2}</span>
但是沒辦法將 state1
與 state2
拆出來給外部:
js
// 假的,沒有這種 APIexport const useStates = () => {const state1 = makeReactive(0);const state2 = makeReactive(1);return [state1, state2];};
html
// 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 變數來簡化元件內的狀態。