雖然疫情的狀況目前尚未明朗,但是該要做的事情還是得持續下去才行。身邊絕大部分的人都已經 WFH 了,所以在家裡看一點新的東西也是挺合理的。
是說,Vue3 這件事情也不新了。很多東西 Kuro 都已經寫在書裡面,所以這邊我就不贅述太多基礎的東西。
Competition API - setup
首先,跟 2.x 最大的差異是這個。在 3.x 裡面起手式大概就 setup () 這件事情,根據官方對於這件事情的說法,大抵上的差異在於,這個 setup () 是介於 created 跟 beforeCreate 中間,所以大多數的特性跟 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 裡面,要定義資料需要使用 ref 或 reactive 來做,這兩者比較大的差異在於,
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這樣的東西,所以一旦解構相關的物件,原有的響應式效果就會消失。
在這些情況下,你就可以使用 toRef 或 toRefs 來進行操作。這邊舉兩個比較常見的例子來看。
第一個是關於 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() 來拿到數值。
小結
以上的程式碼都沒經過測試,請不要胡亂服用。後續還會有一些奇奇怪怪的東西,再找時間跟大家分享一下。疫情其間,請大家注意身體健康、保持清潔,口罩請戴好戴滿,沒事請不要在外面群聚謝謝大家。