Vue3又出新語法 到底何時才能折騰完?

手撕紅黑樹發表於2021-12-25

前言

大家應該知道如果用 Vue3Composition API 定義一個響應式變數通常有兩種形式,一種是用ref,另一種是reactive

<script setup>
import { ref, reactive } from 'vue'

const isLoading = ref(true)

const user = reactive({
  name: '令狐沖',
  age: 22,
  gender: '男'
})
</script>

一般來說定義一個基本資料型別會用ref,而引用型別則會採用reactive,那麼問題來了,ref雖然定義了一個基本資料型別,但實際上它卻是一個引用型別,取值和賦值時必須要帶上.value屬性:

<script setup>
import { ref } from 'vue'

const isLoading = ref(true)

if (isLoading.value) {
  isLoading.value = false
}
</script>

這就有點不太符合直覺了,很有可能一不小心就被寫成了這樣:

<script setup>
import { ref } from 'vue'

let isLoading = ref(true)

if (isLoading) {
  isLoading = false
}
</script>

這要是有TSESLint的加持還好,要是沒有的話可就不好找錯誤了,也不會產生什麼有用的報錯資訊,而且每次都要帶上這個.value實在是不好看,而且寫起來也麻煩呀!

reactive的弊端是不能解構,解構就會失去響應性:

<script setup>
import { reactive } from 'vue'

const user = reactive({
  name: '令狐沖',
  age: 22,
  gender: '男'
})

// 這種寫法通常達不到預期的效果
let { age } = user
age = 18
</script>

可能有人會說,不是有toRefs嗎?用了toRefs,就又會回到那個.value的問題上了:

<script setup>
import { reactive, toRefs } from 'vue'

const user = reactive({
  name: '令狐沖',
  age: 22,
  gender: '男'
})

let { age } = toRefs(user)
age.value = 18
</script>
其實我個人覺得還好啦,因為已經寫習慣了,再加上一直用TS有提示和自動補全,所以感覺沒什麼問題。

但知乎上類似於《為什麼 vue3 刪不掉 ref() 這樣冗餘的函式,但 svelte 可以?》這種問題深深的刺痛了大佬的內心,大佬自己的強迫症也犯了,畢竟他當年創造Vue的最成功要素之一就是方便。而如今這種冗餘的寫法卻與方便毫不搭邊兒,所以尤大無論如何也必須要解決這個問題,不能讓人背後嚼耳根子說Vue寫起來還沒Svelte方便是不是?於是乎大佬先後建立了三次不同的語法糖,它們分別是:

我們先來簡單的看一下,這三次語法糖的寫法:

第一波語法糖

第一波主要是模仿了Svelte的寫法,我們先來看看Svelte的中文官網給出來的一段例子:

<script>
export let title;

// 這將在“title”的prop屬性更改時更新“document.title”
$: document.title = title;

$: {
  console.log(`multiple statements can be combined`);
  console.log(`the current title is ${title}`);
}
</script>

這個$:是一種叫做label的語法,這種語法並不是Svelte自創的語法,而是一種長期在被廢棄的邊緣上瘋狂試探的合法語法,只不過這種語法原本並不是這麼用的,人家是用在巢狀迴圈上的:

let num = 0

outermost:
for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            continue outermost
        } else {
            console.log(i, j, 88)
        }
        num++
    }
}

console.log(num) //95

看不懂沒關係啊,也沒必要去弄懂這種語法,因為它不夠直觀,用處也不是很大,所以幾乎沒什麼人用它!我在編輯器寫這段程式碼的時候 ESLint 都直報錯:

翻譯:Label語法源於GOTO語句,使用它將會令程式碼變得難以理解、難以維護。—ESLint

不過既然沒什麼人在用,同時它還是JS的合法語法,那用它來告訴編譯器這裡是宣告瞭一個ref變數豈不是很完美?於是乎尤大也搞了個和Svelte類似的語法:

<script setup>
ref: isLoading = true

if (isLoading) {
  isLoading = false
}
</script>

那麼大家為何會如此反對呢?就是因為label語法壓根兒就不是這麼用的,人家原本是為了和breakcontinue配合使用的,雖然在別的地方用也不算是語法錯誤,但你這麼做明顯是修改了JS原本的語意!雖然尤大表示很不服啊:為什麼Svelte用這玩意你們都沒說啥,我一用這玩意你們就開噴?!

個人感覺是因為Svelte從一開始就說自己是一個編譯器,沒有沉重的歷史包袱,而Vue卻恰恰相反。而且Svelte本身也不是什麼主流框架,屬於給那幫愛折騰的人玩的。但Vue不一樣,已經有多少人要靠著Vue吃飯呢,並不是所有人都那麼愛折騰的。

於是在萬般無奈之下,尤大隻好放棄了這個提案,但這件事在尤大心裡始終還是揮之不去、如鯁在喉,於是乎他吸取了第一波語法糖的教學,卷頭重來又起草了一份新提案:

第二波語法糖

<script setup>
let loading = $ref(true)

if (loading) {
  loading = false
}
</script>

可以看到我們並沒有引入$ref這個變數,這個變數是從哪來的的呢?是隻要在<script>標籤裡寫了setup這個屬性就會自動注入的一個全域性變數(需要先開啟實驗性語法開關

尤大心想:你們不是嫌我之前用了不規範的語法麼?那我這回這麼寫應該沒問題了吧!想想之前我們定義一個ref變數,首先需要先把ref引進來然後才能用:

import { ref } from 'vue'

const loading = ref(true)

而新語法不用引,直接就能用,類似於全域性變數的感覺。除了$ref這個特殊的全域性變數呢,這次提案還有:$computed$fromRefs$raw這幾個玩意。我們一個個來看,先看$computed

<!-- 以前 -->
<script setup>
import { ref, computed } from 'vue'

const num = ref(1)
const num_10 = computed(() => num.value * 10)
</script>

<!-- 現在 -->
<script setup>
let num = $ref(1)
const num_10 = $computed(() => num * 10)
</script>

$fromRefs又是個啥呢?這玩意在之前沒有啊!只聽說過toRefs

<script setup>
import { fromRefs } from 'vue' // 這個API並不存在
import { toRefs } from 'vue' // 這個API倒是有 也就是隻有 to 沒有 from
</script>

其實這個$fromRefs正是為了配合toRefs而產生的,比方說我們在別的地方寫了一個useXxx

import { reactive } from 'vue'

const state = reactive({
    x: 0,
    y: 0
})

export default = (x = 0, y = 0) => {
    state.x = x
    state.y = y
    
    return toRefs(state)
}

於是我們在使用的時候就:

<script setup>
import { useXxx } form '../useXxx.js'

const { x, y } = useXxx(100, 200)

console.log(x.value, y.value)
</script>

這豈不是又要出現尤大最不想看到的.value屬性了嗎?所以$fromRefs就是為了解決這個問題而生的:

<script setup>
import { useXxx } form '../useXxx.js'

const { x, y } = $fromRefs(useXxx(100, 200))

console.log(x, y)
</script>

最後一個 API 就是$raw了,raw 不是原始的意思嘛!那麼看名字也能猜到,就是我們用$ref所建立出來的其實是一個響應式物件,而不是一個基本資料型別,但語法糖會讓我們在使用的過程中像是在用基本資料型別那樣可以改來改去,但有時我們想看看這個物件長什麼樣,那麼我們就需要用到$raw了:

<script setup>
const loading = $ref(true)

console.log(loading) // 其實列印的不是 loading 這個物件 而是它裡面的值 相當於 loading.value
console.log($raw(loading)) // 這回列印的就是 loading 這個物件了
</script>

改進版

這一版語法糖沒過多久就又被改進了,改進版主要是把全域性變數改為只有$$$這倆變數了,假如我們不用語法糖時是這麼寫:

<script setup>
import { ref } from 'vue'

const loading = ref(true)

console.log(loading.value)
</script>

用語法糖以後就變成了這樣:

<script setup>
import { ref } from 'vue'

const loading = $(ref(true))

console.log(loading)
</script>

如果我們想還原 loading 這個變數,我們就需要用到$$了:

<script setup>
import { ref } from 'vue'

let loading = $(ref(true))

console.log($$(loading))
</script>

或者也可以寫成這樣:

<script setup>
import { ref } from 'vue'

const loadingRef = ref(true)
let loading = $(loadingRef)

console.log(loadingRef === $$(loading))
</script>

第三波語法糖

第三波語法糖主要是在第二波語法的基礎上又進行了改進,除了許多人覺得要寫成$(ref())的話實在是太那什麼了…

另一方面則是實現了props的語法糖,新的語法主要是為每個能夠建立帶有.value變數的方法都有一個$字首的等價物,比如:

  • ref
  • computed
  • shallowRef
  • customRef
  • toRef

與此同時保留了改進版中的$變數與$$變數,用於對props的解構:

<script setup>
const { isLoading } = $(defineProps({ isLoading: Boolean }))
</script>

要知道在以前我們是不能對props進行解構的,而現在還可以利用ES6的解構預設值寫法來為props設定預設值:

<!-- 以前 -->
<script setup>
const props = defineProps({
  isLoading: {
    type: Boolean,
    default: true
  }
}))

console.log(props.isLoading)
</script>

<!-- 現在 -->
<script setup>
const { isLoading = true } = $(defineProps({ isLoading: Boolean }))

console.log(isLoading)
</script>

三波語法糖提案地址

這個框架明明是中國人用的最多,但可笑的是居然是一群外國人在商量Vue的下一步計劃,看到這裡肯定有人會說:中國人都忙著996呢,哪有空去探討那些東西…

那就看你是覺得:這些亂七八糟的語法糖對你來說無所謂,出什麼語法我學什麼就是了,我就是一隻沉默的羔羊

還是說:你只是在這篇文章的下面留個言說自己喜歡這些新語法或者討厭這些新語法,懶得去GitHub說英文。

連結已經給大家貼上來了,就看大家是一副湊熱鬧的態度,還是點進去連結勇敢的表達出自己的聲音了。當然,如果去GitHub我們還是要說英文的,雖說用中文的話尤大也可以看得懂,但評論區不全是中國人,Vue還是有相當一批外國粉絲的。而且也不全是美國人,那些不是英國人美國人的開發者,他們如果也只圖自己痛快而說自己國家的母語的話,想必我們就沒有辦法進行溝通了,同時這也會進一步拉近國人在海外的形象:別人都用英文,就你們中國人用自己的語言,不遵守規則。

那可能有人英文水平真的很差,我們可以這樣嘛:找到百度翻譯,輸入中文後翻譯成英文,然後再把英文複製過去。雖然這樣做翻譯的可能不完全準確,但最起碼能達到勉強看懂的地步。同時還有一個技巧就是把翻譯成英文的句子再翻譯回中文,看看有哪些地方的語意發生了明顯的變化,我們再針對那個地方重新自己寫一遍。

如果你喜歡這個語法,那就去多點幾個贊多誇幾句,這樣的話想必它很快就會被納入到Vue的標準語法裡面去。

如果你不喜歡,那麼就趕快去多噴幾句,這樣的話這個語法很有可能就會像第一波語法糖提案那樣被放棄掉了。

如果你覺得無所謂,愛什麼樣什麼樣,去GitHub一趟多麻煩啊,直接在這篇文章下發表評論多方便。那麼也歡迎你在評論區下留言。

本文首發於公眾號:前端學不動

相關文章