Vue3 - 每天來一點雷 Part 1

雖然疫情的狀況目前尚未明朗,但是該要做的事情還是得持續下去才行。身邊絕大部分的人都已經 WFH 了,所以在家裡看一點新的東西也是挺合理的。

是說,Vue3 這件事情也不新了。很多東西 Kuro 都已經寫在書裡面,所以這邊我就不贅述太多基礎的東西。


Competition API - setup

首先,跟 2.x 最大的差異是這個。在 3.x 裡面起手式大概就 setup () 這件事情,根據官方對於這件事情的說法,大抵上的差異在於,這個 setup () 是介於 createdbeforeCreate 中間,所以大多數的特性跟 created 很類似。

換句話說,這邊是沒有 this 可以用的。再者,如果是 React 的開發者,可能會對這樣的作法很眼熟。具體來說,這個 setup () 設定了整個元件所需要的東西,然後用 return 把資料返回。

import { ref } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = ref(0)
        
        return {
            myVariable
        }
    }
}

以上是一個簡單的例子,我們定義一個 myVariable 並且指定為 ref(0) 然後將他返回。這樣一來,你可以在樣版區塊當中,使用 myVariable 來呈現這個資料。想當然爾,你也可以設定一個方法(函式),用以取代原本 Option API 當中的 methods 設定。

import { ref } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = ref(0)
        
        const myCalculate = () => {
            return 0
        }
        
        return {
            myVariable,
            
            myCalculate
        }
    }
}

在 Vue 當中,你還是可以保留原有 methods 的作法,這並不太影響,至於後續他是否會移除這樣的相容性,就看看官方是否會在更新的版本中將兩種相容作法給移除。只是要留意的是,當你做在 setup () 當中,就沒有 this 可以使用,這一點請務必留意。


ref, reactive 響應式狀態

我們在 3.x 裡面,要定義資料需要使用 refreactive 來做,這兩者比較大的差異在於,

  • ref() 可接受任意資料型態(建議以下七種),但是不會對內容物件做深層監聽。
    • String
    • Number
    • BigInt
    • Boolean
    • Symbol
    • Null
    • Undefined
  • reactive() 僅接受物件(或陣列),會對內容物件做深層監聽。

再者,ref() 所定義的物件,需要使用 .value 來取出對象值,而 reactive() 則不需要。這一點在樣版渲染區塊則沒有差異,3.x 在樣版區塊中會幫你直接提取 ref() 的值出來,也就是說,你在樣版區塊,倘若是 ref() 賦予的值,並不需要額外加上 .value 來提取。

換句話說,當你是在程式端要取用 ref() 的時候,請記得加上 .value,或者使用 unref() 這個糖果語法來幫你拿東西。

import { ref, unref } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = ref(0)
        
        const myCalculate = () => {
            return myVariable.value
            
            // 或者使用 unref 來取值
            return unref(myVariable)
        }
        
        return {
            myVariable,
            
            myCalculate
        }
    }
}

接著來說,既然 ref() 沒有深層監聽,那是否就不能監聽變化?也可以,請使用 computed() 方法來組合變化。例如,

import { ref, computed } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const firstName = ref('Hina')
        const lastName = ref('Chen')
        
        const myName = computed(() => {
            return `${firstName.value} ${lastName.value}`
        })
        
        return {
            myName
        }
    }
}

但這樣挺惱人的,這個時候可以使用 reactive() 來做到相同的事情,

import { reactive, computed } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = reactive({
            firstName: 'Hina',
            lastName: 'Chen',
            myName: computed(() => {
                return `${myVariable.firstName} ${myVariable.lastName}`
            })
        })
        
        return {
            myVariable
        }
    }
}

toRef, toRefs 做了什麼事情

當我們在 ref()reactive() 操作面向趨於複雜的時候,多半會有類似這樣的操作出現,

const { a, b } = useOtherMethodToDoSomething()

好,問題點在於,物件解構的同時,如果你的 useOtherMethodToDoSomething() 沒處理好,會失去響應式的資料結構,所以你就會發現原本寫在同一個元件裡面好好的程式碼,搬去外面之後怎麼突然就失去作用了。追究其原因可以稍微去爬一下 Vue3.x 的原始碼,針對 RefImpl 部分的操作,網路上可以找到一些資料,在此不贅述。

由於 3.x 大量使用 Proxy API,所以你可以發現無論是 ref() 或是 reactive() 所產生出來的對象,每一個都是 Proxy 物件,當你直接解構這個物件,會有一個很模糊又很可愛(?)的現象。

  • ref() 本身解構的話不影響,具體來說核心的 RefImpl 將他賦予了 .value 來監聽數值,所以在這個情況下,不會失去原有響應式的效果。
  • reactive() 的狀況就不同了,由於賦值的方式沒有了 .value 這樣的東西,所以一旦解構相關的物件,原有的響應式效果就會消失。

在這些情況下,你就可以使用 toReftoRefs 來進行操作。這邊舉兩個比較常見的例子來看。

第一個是關於 props 從老爸傳給子物件時,你若是要維持與父元件相同的響應式關連,這樣的操作就很有用。

import { reactive, computed, toRef } from 'vue'

export default {
    name: 'ExampleComponent',
    props: {
        firstName: {
            type: String,
            default: ''
        }
    },
    setup (props) {
        const myVariable = reactive({
            firstName: toRef(props, 'firstName'),
            lastName: 'Chen',
            myName: computed(() => {
                return `${myVariable.firstName} ${myVariable.lastName}`
            })
        })
        
        return {
            myVariable
        }
    }
}

第二個也可以用 props 來解釋,你就把他想成 toRefs() 是把你指定的物件全部都做一次 toRef(),讓他保持響應式的結構。

import { reactive, computed, toRefs } from 'vue'

export default {
    name: 'ExampleComponent',
    props: {
        firstName: {
            type: String,
            default: ''
        }
    },
    setup (props) {
        const { firstName } = toRefs(props)
        
        // 請注意,這邊的 `firstName` 基本上就是做了一次 `toRef()`
        // 所以後面取值出來的時候請不要忘記加上 `.value`
        
        const myVariable = reactive({
            firstName: firstName.value,
            lastName: 'Chen',
            myName: computed(() => {
                return `${myVariable.firstName} ${myVariable.lastName}`
            })
        })
        
        return {
            myVariable
        }
    }
}

useOtherMethodToDoSomething() 沒處理好的東西

剛才已經提到了 toRef()toRefs() 這兩件事情,然後,多半把方法搬出來之後就會忘掉。我們從頭開始講一次需要處理的環節。

import { ref, onMounted, onBeforeUnmount } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const width = ref(0)
        const height = ref(0)
    
        const onWindowResize = (e) => {
            width.value = e.target.innerWidth
            height.value = e.target.innerHeight
        }
    
        onMounted(() => {
            window.addEventListener('resize', onWindowResize, false)
        })
        
        onBeforeUnmount(() => {
            window.removeEventListener('resize', onWindowResize, false)
        })
        
        return {
            width,
            height
        }
    }
}

一開始這樣寫沒有什麼毛病,當這樣的事情越寫越多的時候,就會想把他拿到外面去。

import { ref, onMounted, onBeforeUnmount } from 'vue'

export function useWindowResize() {
    const width = ref(0)
    const height = ref(0)

    const onWindowResize = (e) => {
        width.value = e.target.innerWidth
        height.value = e.target.innerHeight
    }

    onMounted(() => {
        window.addEventListener('resize', onWindowResize, false)
    })

    onBeforeUnmount(() => {
        window.removeEventListener('resize', onWindowResize, false)
    })

    return { width, height }
}

然後我們再回到 Vue 檔案當中把他拿回來用,

import { ref } from 'vue'
import { useWindowResize } from './useWindowResize.js'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const { width, height } = useWindowResize()
        
        const pos = ref({
            width: width,
            height: height
        })
        
        return {
            pos
        }
    }
}

到此沒毛病。原因在於 ref() 本身在 Vue 實作上把值放到 .value 做監聽動作,所以這邊並不會因此而破壞了原有的響應式結構。接著我們來看看一些會破壞的例子,

import { reactive, onMounted, onBeforeUnmount } from 'vue'

export function useWindowResize() {
    const pos = reactive({
        width: 0,
        height: 0
    })

    const onWindowResize = (e) => {
        pos.width = e.target.innerWidth
        pos.height = e.target.innerHeight
    }

    onMounted(() => {
        window.addEventListener('resize', onWindowResize, false)
    })

    onBeforeUnmount(() => {
        window.removeEventListener('resize', onWindowResize, false)
    })

    return pos
}

然後拿回來用的時候,

import { ref } from 'vue'
import { useWindowResize } from './useWindowResize.js'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const { width, height } = useWindowResize()
        
        const pos = ref({
            width: width,
            height: height
        })
        
        return {
            pos
        }
    }
}

你怎麼拿都會拿到 width: 0, height: 0 這樣的結果。主要原因在於 reactive() 在解構的同時,會失去響應式的特性,所以原本使用 reactive() 在 Vue 裡面是可以運作的,當你挪出去的時候,就必須用 toRefs() 把他建立成響應式的樣子。

import { reactive, toRefs, onMounted, onBeforeUnmount } from 'vue'

export function useWindowResize() {
    const pos = reactive({
        width: 0,
        height: 0
    })

    const onWindowResize = (e) => {
        pos.width = e.target.innerWidth
        pos.height = e.target.innerHeight
    }

    onMounted(() => {
        window.addEventListener('resize', onWindowResize, false)
    })

    onBeforeUnmount(() => {
        window.removeEventListener('resize', onWindowResize, false)
    })

    return toRefs(pos)
}

處理 Array 的小毛病

由於 ref()reactive() 基本上都已經被包上一層 Proxy API 了,所以,當你用 console.log 去印出來的時候,已經都是 Proxy 物件,所以,原本針對變數本身的陣列操作就必須要額外小心。

其實也沒什麼特別的地方,如果你確定你的目標物件 一定是陣列 的話,這樣操作即可,

const target = reactive({
    list: []
})

Array.from(target.list).forEach(item => { ... })

如果有經過 toRef() 或是 toRefs() 操作,記得加上 .value 或是使用 unref() 來拿到數值。


小結

以上的程式碼都沒經過測試,請不要胡亂服用。後續還會有一些奇奇怪怪的東西,再找時間跟大家分享一下。疫情其間,請大家注意身體健康、保持清潔,口罩請戴好戴滿,沒事請不要在外面群聚謝謝大家。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei