2020 年 9 月,它終于來了,盡管很早就上了 Beta 版本,但是尤大確一直沒有推出正式版,如今正式版上線,讓我們來一睹為快,看看 Vue3 帶來了哪些改變吧!
Vue3 文檔地址:https://v3.cn.vuejs.org/
Composition API
setup
什么是
setup?vue3之前我們的代碼邏輯會在組件的各個角落,大量碎片化的代碼使得理解和維護組件變得異常困難,在處理單個邏輯關(guān)注點時,我們必須不斷地跳轉(zhuǎn)相關(guān)代碼的選項塊。試想一下如果我們能夠?qū)⑼粋€邏輯的相關(guān)代碼都配置在一起,是不是更容易理解和維護,那么我們將這些邏輯和代碼寫在哪里了?在vue3中,我們將此位置稱為setup。
setup 執(zhí)行時,組件實例尚未被創(chuàng)建 (beforeCreate - created),因此在 setup 中沒有 this 。setup 選項應(yīng)該是一個接收 props 和 content 的函數(shù),我們這里先來看看 props 的用法,在 setup 函數(shù)中除了 props 之外,我們無法訪問組件中聲明的任何屬性(本地狀態(tài)、計算屬性或方法)。直接上栗子吧:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<test username='張三' />
`
})
app.component('test', {
props: ['username'],
setup(props) {
console.log(props) // Proxy {username: "張三"}
// console.log(this) // window
},
template: `<div>{{username}}</div>`
})
const vm = app.mount('#root')
</script>
</html>
上述代碼中,我們創(chuàng)建全局組件 test , 父組件往 test 上傳入 username,test 中通過 props 接收父組件傳遞過來的 username,此時在子組件的 setup 執(zhí)行時可以接收到 props 中的值,但是打印出 this 是指向 window 的。
但是 setup 函數(shù)返回的所有內(nèi)容都將暴露給組件的其余部分(計算屬性、方法、生命周期鉤子等等)以及組件的模板。我們還是在剛剛的 test 組件中來驗證下這句話:
app.component('test', {
props: ['username'],
mounted() { // mounted 聲明周期中可以使用 setup 中返回出來的 username
console.log(this.username) // 張三
},
setup(props) {
const username = props.username
return { username }
},
template: `<div>{{username}}</div>` // 張三
})
const vm = app.mount('#root')
當(dāng)然我們也可以在組件的生命周期或者方法中直接訪問 setup 函數(shù)里面的方法,栗子如下:
const app = Vue.createApp({
mounted() {
// 可以直接在生命周期里面調(diào)用 setup() 里面的方法
this.$options.setup().handleClick()
},
setup(props, context) {
return {
handleClick() {
alert('setup')
}
}
}})
總結(jié):執(zhí)行
setup()是在created()之前執(zhí)行,由于尚未創(chuàng)建實例,所以setup()中無法直接使用this,但是methods/computed/watch等其他地方都可以直接調(diào)用setup()。并且setup()返回的所有內(nèi)容都將暴露給組件的其余部分(計算屬性、方法、生命周期鉤子等等)以及組件的模板。
ref 語法
在 vue3 之前,我們只需要在 data 中定義變量,那么這個變量在 methods/computed/watch 等中的改變就會直接響應(yīng)到組件模板中了。但是在 vue3 之中我們只有通過 ref 函數(shù)和 reactive 函數(shù)來實現(xiàn)響應(yīng)式變量在組件任何地方起作用。我們先通過小栗子可以了解 ref 函數(shù)的基本用法:
const app = Vue.createApp({
setup() {
// 通過 ref 初始化一個響應(yīng)式變量 count 并將其初始值設(shè)為 0
const { ref } = Vue
let count = ref(0)
const increase = () => {
return count.value += 1
}
return { count, increase }
},
template: `
<div>{{count}}</div>
<button @click="increase">增加</button>
`
})
通過上述代碼我們可以看到 ref 接受參數(shù),并將其包裹在一個帶有 value property 的對象中返回,然后可以使用該 property 訪問或更改響應(yīng)式變量的值。那么問題來了為什么要將值封裝在一個對象中呢?這是因為在 JavaScript 中,Number 或 String 等基本類型是通過值傳遞的,而不是通過引用傳遞的。我們知道值傳遞一般指在調(diào)用函數(shù)時將實際參數(shù)復(fù)制一份傳遞到函數(shù)中,這樣如果在函數(shù)中如果對參數(shù)進(jìn)行修改,不會影響到實際參數(shù),既占用內(nèi)存空間也無法實現(xiàn)數(shù)據(jù)的響應(yīng)式。而通過引用傳遞其實是直接把內(nèi)存地址傳過去,也就是說在引用傳值的過程中操作的都是源數(shù)據(jù),也就能更好的實現(xiàn)數(shù)據(jù)響應(yīng)式驅(qū)動。
其實簡單點就是基礎(chǔ)類型的值都是存在棧中,引用類型的值都是存在堆中,堆共用一個數(shù)據(jù)源,而每個棧都是一個獨立的空間,所以我們將數(shù)據(jù)存入堆中才能更好的做到數(shù)據(jù)響應(yīng)式改變。
reactive 語法
reactive 語法和 ref 都是支持?jǐn)?shù)據(jù)的響應(yīng)式,但是 reactive 接收的一般是數(shù)組和對象,直接來看小栗子:
const app = Vue.createApp({
setup() {
// 通過 reactive 初始化一個響應(yīng)式對象
const { reactive, computed } = Vue
const data = reactive({
count: 0,
increase: () => data.count++,
docuble: computed(() => data.count * 2)
})
return { data }
},
template: `
<div>{{data.count}}</div>
<div>{{data.docuble}}</div>
<button @click="data.increase">增加</button>
`
})
上述代碼中我們會發(fā)現(xiàn)模板中每次都要寫 data.count 、data.docuble 過于繁瑣,如果我們在 return { data } 時使用展開運算符或者對象賦值的方式導(dǎo)出,是否可以直接使用 count 、 docuble 等變量呢。
// 使用展開運算符
return {...data}
// 使用對象賦值
return {
count: data.count,
increase: data.increase,
docuble: data.docuble
}
我們發(fā)現(xiàn)取出來之后,count、increase、docuble 都喪失了響應(yīng)式的活性,他們變成了不同的 javascript 類型。那么我們?nèi)绾谓鉀Q這種情況呢?vue3 為我們推出了 toRefs 函數(shù)。
toRefs 語法
我們先試用 toRefs 改寫上面的代碼:
const app = Vue.createApp({
setup() {
// 通過 reactive 初始化一個響應(yīng)式對象
const { reactive, computed, toRefs } = Vue
const data = reactive({
count: 0,
increase: () => data.count++,
docuble: computed(() => data.count * 2)
})
// toRefs 將 data 轉(zhuǎn)變?yōu)橐粋€響應(yīng)式對象
const { count, increase, docuble } = toRefs(data)
return { count, increase, docuble }
},
template: `
<div>{{count}}</div>
<div>{{docuble}}</div>
<button @click="increase">增加</button>
`
})
toRefs 函數(shù)接收一個 reactive 對象作為參數(shù),返回一個普通的對象,但是這個普通對象的每一項都變成了 ref 類型的對象,即支持響應(yīng)式。
1、
ref / reactive通過proxy對數(shù)據(jù)進(jìn)行封裝,當(dāng)數(shù)據(jù)變化時,觸發(fā)模板等內(nèi)容的更新。
2、ref處理基礎(chǔ)類型的數(shù)據(jù)。例如ref('cc')變成proxy({value: 'chen'})這樣一個響應(yīng)式的引用。
3、reactive處理非基礎(chǔ)類型的數(shù)據(jù)。例如reactive({name: 'cc'})變成proxy({name: 'cc'})這樣一個響應(yīng)式引用。
4、toRefs接收一個reactive對象作為參數(shù),返回一個普通對象,并將普通對象的每一項都變成了ref類型的對象,保持器響應(yīng)式活力。例如:toRefs proxy({name: 'cc'})變成{name: proxy({value: 'cc'})}。
readonly 語法
獲取一個對象 (響應(yīng)式或純對象) 或 ref 并返回原始 proxy 的只讀 proxy。舉個栗子:
const app = Vue.createApp({
setup(props, context) {
const { reactive, readonly, toRefs } = Vue
let nameObj = reactive([123])
// 獲取 nameObj 這個響應(yīng)對象為參數(shù)
let copyNameObj = readonly(nameObj)
setTimeout(() => {
nameObj[0] = 456
// 因為使用了 readonly 無法改變 copyNameObj 的值
copyNameObj[0] = 456
}, 2000)
return { nameObj, copyNameObj }
}
})
toRef 語法
和 toRefs 只差了一個 s,那么它有什么用呢?讓我們先來假設(shè)一個場景,如下栗子:
const app = Vue.createApp({
template: `
<div>{{age}}</div>
`,
setup(props, context) {
const { reactive, toRefs, toRef } = Vue
const data = reactive({ name: 'zhangsan' })
const { age } = toRefs(data)
console.log(age) // undefined
setTimeout(() => {
age.value = 20 // 報錯 Cannot set property 'value' of undefined
}, 2000)
return { age }
}
}).mount('#root')
我們定義了一個響應(yīng)式變量 data ,toRefs 會將相應(yīng)對象解析成一個普通對象,并保證該普通對象的屬性都是 ref 響應(yīng)式屬性,我們又想在新增加一個 age 的響應(yīng)式變量,結(jié)果只得到了一個 undefined 類型而不是預(yù)料中的 ref 類型。此時我們可以使用 toRef 改造上面的代碼:
const app = Vue.createApp({
template: `
<div>{{age}}</div>
`,
setup(props, context) {
const { reactive, toRefs, toRef } = Vue
const data = reactive({ name: 'zhangsan' })
const age = toRef(data, 'age')
setTimeout(() => {
age.value = 20
}, 2000)
return { age }
}
}).mount('#root')
toRef 可以用來為源響應(yīng)式對象上的某個 property 新創(chuàng)建一個 ref,這里我們就新建了一個 ref 類型的 age 變量,然后 age 就可以完美繼承響應(yīng)活性了。
setup 參數(shù) context
前面我們介紹了 setup 函數(shù)中的第一個參數(shù) props,這里我們再來看看它的第二個參數(shù) context 的用法,根據(jù)官網(wǎng)文檔傳遞給 setup 函數(shù)的第二個參數(shù)是 context。context 是一個普通的 JavaScript 對象,它暴露三個組件的 property:
export default {
setup(props, context) {
const { attrs, slots, emit } = context
}
}
context 是一個普通的 JavaScript 對象,也就是說,它不是響應(yīng)式的,這意味著你可以安全地對 context 使用 ES6 解構(gòu)。
export default {
setup(props, { attrs, slots, emit }) {}
}
那首先我們來看一下 attrs 的使用:
const app = Vue.createApp({
template: `
<div>
<child username="zhangsan"></child>
</div>
`
})
app.component('child', {
template: `
<div></div>
`,
setup(props, context) {
const { attrs, slots, emit } = context
// attrs 接收一個 None-Props 屬性
console.log(attrs.username)
}
})
const vm = app.mount('#root')
什么是 None-Props 屬性 呢?正常情況下,父組件給子組件傳遞了 username ,子組件應(yīng)該通過 props: ['username'] 去接收,但是如果子組件如果不使用 props 接收,那么就可以在 attrs 處接收到 username。而這個 username 就是 None-Props 屬性 。
slots 看名字應(yīng)該就可以大致猜到,這是用來接收插槽的,同樣是上面的栗子,我們改寫一下:
const app = Vue.createApp({
template: `
<div>
<child>parent</child>
</div>
`
})
app.component('child', {
setup(props, context) {
const { h } = Vue
const { attrs, slots, emit } = context
return () => h('div', {}, slots.default())
}
})
const vm = app.mount('#root')
而如果不在 setup 函數(shù)中,而是在組件外部的生命周期函數(shù)中,我們只能通過 this 去獲取 slots ,現(xiàn)在我們可以完全脫離 vue2 的寫法,將所有的東西都聚合到 setup 函數(shù)中。
mounted() {
console.log(this.$slots.default())
},
emit 看名字其實應(yīng)該也可以猜出來,子組件像父組件的事件傳遞,直接看栗子:
const app = Vue.createApp({
template: `
<div>
<child @change="change"></child>
</div>
`,
setup() {
const change = () => {
alert('change')
}
return { change }
}
})
app.component('child', {
template: `
<div @click="handleClick">child</div>
`,
setup(props, context) {
const { attrs, slots, emit } = context
const handleClick = () => {
// 使用 emit 直接傳遞給父組件
emit('change')
}
return { handleClick }
}
})
const vm = app.mount('#root')
watch 語法
熟悉 vue2 的小伙伴對 watch 肯定不陌生,在 vue3 中,我們可以把 watch 集成到 setup 函數(shù)中進(jìn)行使用。watch 需要偵聽特定的數(shù)據(jù)源,并在單獨的回調(diào)函數(shù)中執(zhí)行副作用。默認(rèn)情況下,它也是惰性的——即回調(diào)僅在偵聽源發(fā)生更改時被調(diào)用。
使用 watch 監(jiān)聽單個數(shù)據(jù)源
const app = Vue.createApp({
setup() {
const { ref, reactive, watch } = Vue
let title = ref("")
const editTitle = () => {
title.value = 'hello world'
}
watch(title, (newValue, oldValue) => {
console.log('old', oldValue) // 'old', ""
console.log('new', newValue) // 'new', "hello world"
})
return { title, editTitle }
},
template: `
<div>ref 值監(jiān)聽:{{title}}</div>
<button @click="editTitle">改變title</button>
`
}).mount('#root')
使用 watch 監(jiān)聽多個數(shù)據(jù)源
const app = Vue.createApp({
setup() {
const { ref, reactive, watch } = Vue
let title = ref("")
const data = reactive({
count: 0
})
const editTitle = () => {
title.value = 'hello world'
}
const editCount = () => {
data.count++
}
// watch 可以監(jiān)聽一個數(shù)組,里面是需要監(jiān)聽的多個值
watch([title, data], (newValue, oldValue) => {
console.log('old', oldValue) // ["", Proxy]
console.log('new', newValue) // ["hello world", Proxy]
})
return { title, editTitle, data, editCount }
},
template: `
<div>reactive 值監(jiān)聽:{{data.count}}</div>
<div>ref 值監(jiān)聽:{{title}}</div>
<button @click="editTitle">改變title</button>
<button @click="editCount">改變count</button>
`
}).mount('#root')
上面的栗子中,我們使用監(jiān)聽了 reactive 新增監(jiān)聽了一個 data 對象,但是如果我們只想監(jiān)聽 data 對象中的 count 屬性,而不是監(jiān)聽整個 data 對象,那么我們應(yīng)該如何實現(xiàn)呢?
// watch 中接收一個函數(shù),其返回值為 reactive 函數(shù)對象中想監(jiān)聽的值
watch([title, () => data.count], (newValue, oldValue) => {
console.log('old', oldValue) // ["", 0]
console.log('new', newValue) // ["hello world", 1]
})
watchEffect 語法
watchEffect 和 watch 都是用來對數(shù)據(jù)的偵聽,但是他們有 3 個比較大的差別:
1、
watchEffect立即執(zhí)行,沒有惰性。
2、不需要傳遞你要偵聽的內(nèi)容,自動會感知代碼依賴,不需要傳遞很多參數(shù),只需要傳遞一個回調(diào)函數(shù)。
3、不能獲取之前數(shù)據(jù)的值 。
舉個栗子:
const app = Vue.createApp({
template: `
<input v-model="nameObj.name" />
`,
setup() {
const { reactive, watchEffect } = Vue
const nameObj = reactive({ name: 'cc' })
watchEffect(() => {
// 初次加載就會執(zhí)行打印出 `abc`
// 但是自動檢測到 `abc` 和頁面沒有任何關(guān)系后面就不會在執(zhí)行
console.log('abc')
// nameObj.name 綁定了頁面的 input,所以每次輸入改變 name 的值都會執(zhí)行一次
console.log(nameObj.name)
})
return { nameObj }
}
}).mount('#root')
如果我們要在 5 秒后停止 watch 或者 watchEffect 對屬性的監(jiān)聽?wèi)?yīng)該如何做呢,如下栗子:
const app = Vue.createApp({
template: `
<input v-model="nameObj.name" />
`,
setup() {
const { reactive, watchEffect } = Vue
const nameObj = reactive({ name: 'cc' })
// 將函數(shù)變成一個命名函數(shù),然后 5s 后在執(zhí)行一次這個命名函數(shù)(watch 同理)
const stop = watchEffect(() => {
console.log(nameObj.name)
setTimeout(() => {
stop()
}, 5000)
})
return { nameObj }
}
}).mount('#root')
老瓶新酒 - 生命周期
vue3 組件中的生命周期和 vue2 組件生命周期基本相同,唯一不同的是將 beforeDestroy 和 destroyed 改成了 beforeUnmount 和 unmounted,根據(jù)尤大的說法是后者語義性更強,更能表達(dá)組件卸載的說法。通過代碼簡單回顧下:
// 生命周期函數(shù):在某一時刻會自動執(zhí)行的函數(shù)
const app = Vue.createApp({
data() {
return {
message: 'see you'
}
},
template: `
<div @click="handleClickItem">{{message}}</div>
`,
methods: {
handleClickItem() {
this.message = 'bye bye'
setTimeout(() => {
app.unmount()
}, 1000)
}
},
// 在實例生成之前會自動執(zhí)行的函數(shù)
beforeCreate() {
console.log('beforeCreated', this.message)
},
// 在實例生成之后會自動執(zhí)行的函數(shù)
created() {
console.log('created', this.message)
},
// 在組件內(nèi)容被渲染到頁面之前自動執(zhí)行的函數(shù)
beforeMount() {
console.log('beforeMounte', document.getElementById('root').innerHTML)
},
// 在組件內(nèi)容被渲染到頁面之后自動執(zhí)行的函數(shù)
mounted() {
console.log('mounted', document.getElementById('root').innerHTML)
},
// 在組件內(nèi)容被修改之前自動執(zhí)行的函數(shù)
beforeUpdate() {
console.log('beforeUpdate', document.getElementById('root').innerHTML)
},
// 在組件內(nèi)容被修改之后自動執(zhí)行的函數(shù)
updated() {
console.log('updated', document.getElementById('root').innerHTML)
},
// 在 Vue 應(yīng)用失效時,自動執(zhí)行的函數(shù) 可以通過 app.unmount() 觸發(fā)
beforeUnmount() {
console.log('beforeUnmount', document.getElementById('root').innerHTML)
},
// 當(dāng) Vue 應(yīng)用失效且 Dom 完全銷毀之后,自動執(zhí)行的函數(shù)
unmounted() {
console.log('unmount', document.getElementById('root'))
}
})
const vm = app.mount('#root')
setup 函數(shù)中的調(diào)用生命周期鉤子
- beforeCreate -> 不需要
- created -> 不需要
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeUnmount -> onBeforeUnmount
- unmounted -> onUnmounted
- errorCaptured -> onErrorCaptured
- renderTracked -> onRenderTracked
- renderTriggered -> onRenderTriggered
因為
setup是圍繞beforeCreate和created生命周期鉤子運行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何代碼都應(yīng)該直接在setup函數(shù)中編寫。
vue3 模塊化開發(fā)
前面我們所有的邏輯代碼都寫進(jìn)了 setup 函數(shù)中,雖然所有的代碼都寫在 setup 函數(shù)中是可以將邏輯都聚合到一起,但是如果一個頁面所有的邏輯代碼都寫入一個函數(shù),只會讓代碼顯得更加難以維護。也許你寫完當(dāng)前也頁面的所有邏輯代碼有 1000 行,但是三天之后你在想維護這段代碼,估計你自己也會懵逼,不知從何寫起。所以日常開發(fā)中我們更應(yīng)該將 setup 當(dāng)做一個流程處理函數(shù),將該頁面的所有邏輯抽離成一個一個模塊,將每個模塊的結(jié)果導(dǎo)出到 setup 函數(shù)中供頁面模板使用。我們先來看一個簡單的栗子:
我們這里實現(xiàn)一個簡單的功能,就是記錄每次鼠標(biāo)點擊屏幕時打印出鼠標(biāo)當(dāng)前的坐標(biāo)值!
// 將鼠標(biāo)坐標(biāo)值更新邏輯抽離成一個單獨的函數(shù)
const { reactive, toRefs, onMounted, onUnmounted } = Vue
const updateMouseEffect = () => {
const mouseObj = reactive({
x: 0,
y: 0
})
const updateMouse = (e) => {
mouseObj.x = e.pageX
mouseObj.y = e.pageY
}
onMounted(() => {
document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
document.removeEventListener('click', updateMouse)
})
const { x, y } = toRefs(mouseObj)
return { x, y }
}
const app = Vue.createApp({
// 頁面上的每一個邏輯都抽離成對應(yīng)的函數(shù)
// setup 函數(shù)只負(fù)責(zé)將封裝的邏輯中導(dǎo)出的值引入,不涉及具體邏輯編寫
setup() {
const { x, y } = updateMouseEffect()
return { x, y }
},
template: `
<h1>x: {{x}} y: {{y}}</h1>
`
}).mount('#root')
看了上面的代碼,是不是覺得 vue3 可以完美實現(xiàn)各種 hooks ,接下來我們結(jié)合 axios 來實現(xiàn)一個涉及外部請求的封裝函數(shù)。這里都是用的 cdn ,如需復(fù)現(xiàn)栗子,記得頭部引入 axios 的 cdn 鏈接。
const { ref } = Vue
const useURLLoader = (url) => {
const result = ref(null) // 響應(yīng)結(jié)果
const loading = ref(true) // 是否顯示loading
const loaded = ref(false) // 是否加載完成
const error = ref(null) // 是否響應(yīng)錯誤
axios.get(url).then((rawData) => {
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch((e) => {
error.value = e
})
return { result, loading, loaded, error }
}
const url = 'https://dog.ceo/api/breeds/image/random'
const app = Vue.createApp({
setup() {
const { result, loading, loaded, error } = useURLLoader(url)
return { result, loading, loaded, error }
},
template: `
<h1 v-if="loading">Loading!...</h1>
<img v-if="loaded" :src="result.message" >
`
}).mount('#root')
這個栗子其實就是我們在發(fā)請求時希望頁面顯示 loading ,拿到請求結(jié)果之后將 loading 狀態(tài)取消然后展示我們拿到的結(jié)果數(shù)據(jù),代碼很簡單,也是將所有的邏輯代碼都抽離到一個單獨的函數(shù)中,setup 函數(shù)只負(fù)責(zé)流程控制和數(shù)據(jù)導(dǎo)出。
常用的一些都整理出來了,本來想繼續(xù)整理一些 vue3 中的新特性,但是篇幅太長了,就準(zhǔn)備在下一篇中繼續(xù)整理了,如有錯誤或不正確的地方歡迎指正,每天進(jìn)步一點點,加油?。?!