雖然疫情的狀況目前尚未明朗,但是該要做的事情還是得持續下去才行。身邊絕大部分的人都已經 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()
來拿到數值。
小結
以上的程式碼都沒經過測試,請不要胡亂服用。後續還會有一些奇奇怪怪的東西,再找時間跟大家分享一下。疫情其間,請大家注意身體健康、保持清潔,口罩請戴好戴滿,沒事請不要在外面群聚謝謝大家。