上次將 Composition API
大致梳理了一遍 ,這次主要是想記錄一些 vue3
相較 vue2
新增出來的一些特性和一些方法上使用的變動,話不多說,直接開擼。
Teleport
我們?nèi)粘i_發(fā)中經(jīng)常會遇到這樣一個場景,比如我們封裝一個彈層 msk
組件,但是它包裹在一個 position: relative
定位的父元素中,此時它將被局限在父元素的大小中,我們很難將彈層鋪滿整個窗口。而 Teleport
提供了一種干凈的方法,允許我們控制在 DOM
中哪個父節(jié)點(diǎn)下渲染了 HTML
,而不必求助于全局狀態(tài)或?qū)⑵洳鸱譃閮蓚€組件。舉個栗子:
<body>
<div id="root"></div>
<div id="center"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
<msk></msk>
</div>
`
})
app.component('msk', {
template: `
<div class="msk">111</div>
`
})
app.mount('#root')
</script>
瀏覽器渲染結(jié)果如下:
這肯定不是我們想要實(shí)現(xiàn)的效果,我們希望蒙層是充滿整個窗口的,此時我們可以直接將蒙層組件通過 teleport
渲染到 body
下面或者我們指定的 dom
節(jié)點(diǎn)下面,teleport
上面有一個 to
的屬性,它接受一個 css query selector
作為參數(shù)。如下栗子:
<script>
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
// 使用 to 屬性將其掛載到 id = center 的 dom 節(jié)點(diǎn)下
// 我們也可以直接使用 to = body 將其直接掛載到 body 中
<teleport to="#center">
<msk></msk>
</teleport>
</div>
`
})
</script>
emits
我們知道在 vue2
中父子組件傳值會用到 props
和 $emit
,但是在 vue3 中新增了 emits
,它的主要作用是匯總該組件有哪些自定義事件,可以是一個數(shù)組寫法,也可以是一個對象寫法,同時在對象寫法中還支持自定義函數(shù),可以在運(yùn)行時驗(yàn)證參數(shù)是否正確。
了解它的基礎(chǔ)用法之后我們將 teleport
中寫入的小栗子重寫,讓其組件通信完成最基本的顯示和隱藏的動態(tài)交互功能。當(dāng)然我們在子組件通過 $emit
觸發(fā)的事件要統(tǒng)一寫入 emits
數(shù)組中進(jìn)行管理。
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
<button @click="openMsk">打開彈層</button>
<teleport to="#center">
<msk :isOpen="isOpen" @closeMsk="closeMsk"></msk>
</teleport>
</div>
`,
setup() {
const { ref } = Vue
const isOpen = ref(false)
const openMsk = () => {
isOpen.value = true
}
const closeMsk = () => {
isOpen.value = false
}
return { openMsk, isOpen, closeMsk }
}
})
app.component('msk', {
props: ['isOpen'],
// 子組件中我們會向父組件觸發(fā) `closeMsk` 事件,所以將其統(tǒng)一寫入 `emits` 中方便管理維護(hù)
emits: ['closeMsk'],
template: `
<div class="msk" v-show="isOpen">
<button @click="closeMsk">關(guān)閉彈層</button>
</div>
`,
setup(props, context) {
const closeMsk = () => {
context.emit('closeMsk')
}
return { closeMsk }
}
})
app.mount('#root')
當(dāng)然我們也可以在 emits
中使用對象寫法,并且傳入驗(yàn)證的自定義函數(shù):
app.component('msk', {
props: ['isOpen'],
emits: {
// 'closeMsk': null, 無需驗(yàn)證
'closeMsk': (payload) => {
return payload === 111 // 事件觸發(fā)時驗(yàn)證傳入的值是否為 111
// 驗(yàn)證失敗,因?yàn)槲覀魅氲氖?222
// 無效的事件參數(shù):事件“closeMsk”的事件驗(yàn)證失敗。
}
},
setup(props, context) {
const closeMsk = () => {
context.emit('closeMsk', 222)
}
return { closeMsk }
}
}
小伙伴們可以試一試,當(dāng)然即使我傳入的值和驗(yàn)證時的值不匹配但是并不會影響這個事件的正常執(zhí)行,只是會在瀏覽器中給出警告提示。
Suspense
和 teleport
組件一樣,這也是 vue3.0
新推出來的一個全新的組件,它的主要作用是和異步組件一起使用,我們可以現(xiàn)在這里回憶一下 vue2.0
中我們是如何使用動態(tài)組件和異步組件的。
動態(tài)組件
vue 2.0
和 vue3.0
動態(tài)組件的使用方式基本差不多,都是根據(jù)數(shù)據(jù)的變化,結(jié)合 component
這個標(biāo)簽,來隨時動態(tài)切換組件的實(shí)現(xiàn)。這里簡單做個小回顧:
const app = Vue.createApp({
setup() {
const { ref, keepAlive } = Vue
const currentItem = ref('first-item')
const handleClick = () => {
if (currentItem.value === 'first-item') {
currentItem.value = 'second-item'
} else {
currentItem.value = 'first-item'
}
}
return { currentItem, handleClick }
},
template: `
<keep-alive>
<component :is="currentItem"></component>
</keep-alive>
<button @click="handleClick">組件切換</button>
`
})
app.component('first-item', {
template: `
<div>hello world</div>
`
})
app.component('second-item', {
template: `
<input type="text" />
`
})
app.mount('#root')
異步組件
以前,異步組件是通過將組件定義為返回 Promise 的函數(shù)來創(chuàng)建的,這里可以直接查看 vue2.0 中如何定義異步組件。但是在 vue3.0
中現(xiàn)在,由于函數(shù)式組件被定義為純函數(shù),因此異步組件的定義需要通過將其包裝在新的 defineAsyncComponent 助手方法中來顯式地定義,其實(shí)也很簡單,看栗子就知道了:
const { defineAsyncComponent } = Vue
const app = Vue.createApp({
template: `
<div>
<async-show></async-show>
<async-common-item></async-common-item>
</div>
`
})
app.component('asyncShow', defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve({
template: `<div>我將在 1s 之后被渲染出來</div>`
})
}, 1000)
})
}))
app.component('async-common-item', defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve({
template: `<div>我將在 3s 之后被渲染出來</div>`
})
}, 3000)
})
}))
defineAsyncComponent
接受返回 Promise
的工廠函數(shù)。從服務(wù)器檢索組件定義后,應(yīng)調(diào)用 Promise
的 resolve
回調(diào)。你也可以調(diào)用 reject(reason)
,來表示加載失敗。
接下來就可以引入我們的主角 Suspense
組件,他可以用來接收一個或多個異步組件,它本身支持兩個具名插槽,一個承載異步插件返回等待狀態(tài)的插槽,一個承載異步插件返回成功狀態(tài)的插槽。舉個小栗子:
const { defineAsyncComponent } = Vue
const app = Vue.createApp({
template: `
<Suspense >
<template #default> // 異步組件成功內(nèi)容包裹在 default 插槽中
<async-show></async-show>
</template>
<template #fallback> // 異步組件未加載時顯示 fallback里的內(nèi)容
<h1>loading !!!!</h1>
</template>
</Suspense>
`
})
當(dāng)然 Suspense
組件也支持多個異步組件的插入,并且它會等待所有異步組件都返回才將其顯示出來,不過此時我們需要在其根上包一層,如下栗子:
const app = Vue.createApp({
template: `
<Suspense >
<template #default>
<div>
<async-show></async-show>
<async-common-item></async-common-item>
</div>
</template>
<template #fallback>
<h1>loading !!!!</h1>
</template>
</Suspense>
`
})
Provide / Inject
通常,當(dāng)我們需要從父組件向子組件傳遞數(shù)據(jù)時,我們使用 props。想象一下這樣的結(jié)構(gòu):有一些深度嵌套的組件,而深層的子組件只需要父組件的部分內(nèi)容。在這種情況下,如果仍然將 prop
沿著組件鏈逐級傳遞下去,可能會很麻煩。
對于這種情況,我們可以使用一對 provide
和 inject
。無論組件層次結(jié)構(gòu)有多深,父組件都可以作為其所有子組件的依賴提供者。這個特性有兩個部分:父組件有一個 provide
選項(xiàng)來提供數(shù)據(jù),子組件有一個 inject
選項(xiàng)來開始使用這些數(shù)據(jù)。
上面兩段話摘自官網(wǎng),說的很明白,基礎(chǔ)的用法其實(shí)和 vue2
中差不多,但是我們知道 vue2
中無法實(shí)現(xiàn)數(shù)據(jù)的響應(yīng)式監(jiān)聽,但是 vue3
中我們使用 composition API
就可以完成對應(yīng)響應(yīng)式變化的監(jiān)聽。我們先來回顧一下 vue2
中的基礎(chǔ)用法:
父組件像孫子組件傳遞固定值
const app = Vue.createApp({
provide: {
count: 1
},
template: `
<child />
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
inject: ['count'],
template: `
<div>{{count}}</div>
`
})
如果我們想使用 provide
傳遞數(shù)據(jù) data
中的值時,我們就不能用上面這種寫法,我們需要將 provide
轉(zhuǎn)換為返回對象的函數(shù)。栗子如下:
const app = Vue.createApp({
data() {
return {
count: 1
}
},
provide() {
return {
count: this.count
}
}
})
父組件像孫子組件動態(tài)傳值
如果此時我們新增一個按鈕改變父組件中 count
的值,子組件是無法繼續(xù)監(jiān)聽到改變后的值的。此時如果我們想對祖先組件中的更改做出響應(yīng),我們需要為 provide
的 count
分配一個組合式API computed
const app = Vue.createApp({
data() {
return {
count: 1
}
},
methods: {
handleClick() {
this.count++
}
},
provide() {
return {
count: Vue.computed(() => this.count)
}
},
template: `
<child />
<button @click="handleClick">增加</button>
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
inject: ['count'],
template: `
<div>{{count.value}}</div>
`
})
當(dāng)然此時我們還沒有用到 Composition API
來實(shí)現(xiàn),我們在使用 Composiiton API
來重構(gòu)下上面的代碼:
const { ref, provide, inject } = Vue
const app = Vue.createApp({
setup() {
let count = ref(1)
const handleClick = () => {
count.value++
}
provide('count', count)
return { handleClick }
},
template: `
<child />
<button @click="handleClick">增加</button>
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
setup() {
let count = inject('count')
return { count }
},
template: `
<div>{{count}}</div>
`
})
是不是感覺非常簡單,那么問題來了,剛剛我們使用了 provide / inject
實(shí)現(xiàn)了父組件和子孫組件中的數(shù)據(jù)傳遞,如果兩個毫無關(guān)聯(lián)的組件,那么我們應(yīng)該如何建立數(shù)據(jù)通訊呢?除了 vuex
你最先能想到什么,在 2.x
中,Vue
實(shí)例可用于觸發(fā)通過事件觸發(fā) API
強(qiáng)制附加的處理程序 ($on
,$off
和 $once
),這用于創(chuàng)建 event hub
,以創(chuàng)建在整個應(yīng)用程序中使用的全局事件偵聽器,因?yàn)槲仪懊鎸戇^相關(guān)文章,關(guān)于 vue2
的知識就不在這里過多贅述了,詳情可以點(diǎn)擊 Vue 常見 API 及問題。
但是在 vue3
中廢棄了 $on, $off
,為什么會廢棄呢,可以參考文章解讀Vue3中廢棄組件事件進(jìn)行解讀。官方推薦我們使用第三方庫 mitt
進(jìn)行全局事件的綁定和監(jiān)聽。大體用法其實(shí)和原來差不多,這里就不過多贅述了。
Mixin
Mixin
應(yīng)該算 vue2
中用的比較多的,用法其實(shí)和以前大體相差不大,我在 Vue 常見 API 及問題 中記錄過 Mixin
的基礎(chǔ)用法,官網(wǎng)寫的也挺詳細(xì)的,當(dāng)然現(xiàn)在關(guān)于組件公共邏輯的抽離其實(shí)更推薦使用 組合式 API 。這里還是簡單總結(jié)下 Mixin
的幾個特點(diǎn):
1、混入過程中,組件
data
、methods
、優(yōu)先級高于mixin data
,methods
優(yōu)先級。
2、生命周期函數(shù),先執(zhí)行mixin
里面的,在執(zhí)行組件里面的。
3、自定義的屬性,組件中的屬性優(yōu)先級高于mixin
屬性優(yōu)先級。
什么叫自定義屬性呢?我們來看個小栗子:
const app = Vue.createApp({
number: 3,
template: `
<div>{{number}}</div>
`
})
我們直接定義了一個屬性 number
,它既不在 data
中,也不在 setup
中,而是直接掛載在 app
上,那么它就是 app
上的自定義屬性。此時我們無法在模板中直接使用 this
訪問到這個 number
屬性,必須要通過 this.$options
才能訪問到它:
const app = Vue.createApp({
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
如果我們此時在 mixin
中也定義一個 number
屬性:
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
前面我們說過,mixin
的優(yōu)先級低于組件優(yōu)先級,所以此時肯定輸出的是 3
,但是如果我們希望 mixin
的優(yōu)先級高于組件優(yōu)先級,我們就可以使用 app.config.optionMergeStrategies
自定義選項(xiàng)合并策略:
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
// 接收兩個參數(shù),配置優(yōu)先返回第一個參數(shù),如找不到在返回第二個參數(shù)
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
return mixinValue || appValue
}
自定義指令
我們先假想一個使用場景,如果我們希望在頁面加載的時候自動獲取 input
框的焦點(diǎn)事件,我們一般會這樣寫:
const app = Vue.createApp({
template: `
<input ref="input" />
`,
mounted() {
this.$refs.input.focus()
}
})
假如另一個組件中又有一個 input
,那么我們就又需要在那個組件的 dom
元素節(jié)點(diǎn)處定義 ref
,然后在組件的生命周期中調(diào)用一遍 this.$refs.input.focus()
。如果我們可以定義一個全局的 autofocus
事件,只要遇到 input
我們就通過給定的指令直接觸發(fā)那應(yīng)該怎么辦呢?此時我們就可以用到自定義指令了:
const app = Vue.createApp({
template: `
<input ref="input" v-focus />
`
})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
通過過 app.directive
我們定義了一個全局的 focus
指令,指令的使用只需要在前面加上 v-
即可;當(dāng)然指令也和組件一樣,有著生命周期,我們在 mounted
的時候可以拿到使用指令的 dom
元素節(jié)點(diǎn),然后操作這個節(jié)點(diǎn)完成對應(yīng)的功能。當(dāng)然上面我們使用 app.directive
將指令定義到了全局,日常開發(fā)中我們可能更多的是使用局部指令:
// 定義指令集合,因?yàn)榭梢允嵌鄠€指令,所以是復(fù)數(shù)
const directives = {
focus: {
mounted(el) {
el.focus()
}
}
}
const app = Vue.createApp({
directives,
template: `
<input ref="input" v-focus />
`
})
動態(tài)指令參數(shù)
例如我們想通過一個指令實(shí)時改變 input
框的位置,那么此時我們寫下代碼:
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos />
`
})
app.directive('pos', {
mounted(el) {
el.style.top = '200px'
}
})
上面這個栗子雖然我們每次使用 v-pos
都會改變輸入框的 top
值,但是如果我們希望這個值不是固定的 200
,而是指令中傳給我們的數(shù)字,那該如何進(jìn)行改造呢?
官網(wǎng)的文檔資料告訴了我們,指令中的參數(shù)可以是動態(tài)的,例如,在 v-mydirective:[argument]="value"
中,argument
參數(shù)可以根據(jù)組件實(shí)例數(shù)據(jù)進(jìn)行更新!這使得自定義指令可以在應(yīng)用中被靈活使用。光看文字可能有點(diǎn)糊,我們來使用實(shí)際的栗子:
// 場景一:指令接收傳值
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos="100" />
`
})
app.directive('pos', {
mounted(el, binding) {
// binding.value 是我們傳遞給指令的值——在這里是 200
el.style.top = binding.value + 'px'
}
})
其實(shí)上面的小栗子還有個缺點(diǎn),就是我們將 v-pos
的值定義在 data
中,但是我們實(shí)時改變 data
中的值,頁面并不會產(chǎn)生對應(yīng)的響應(yīng)式變化。那是因?yàn)槲覀冎噶钭缘倪^程中 mounted
生命周期只會執(zhí)行一遍,所以如果我們希望對應(yīng)變化的產(chǎn)生就可以使用 updated
生命周期:
const app = Vue.createApp({
data() {
return {
top: 100
}
},
template: `
<input class="input" v-pos="top" />
`
})
app.directive('pos', {
mounted(el, binding) {
el.style.top = binding.value + 'px'
},
// 通過 updated 監(jiān)聽指令值的實(shí)時變化
updated(el, binding) {
el.style.top = binding.value + 'px'
}
})
const vm = app.mount('#root')
當(dāng)然如果我們在 mounted
和 updated
時觸發(fā)相同行為,而不關(guān)心其他的鉤子函數(shù)。那么你可以通過將這個回調(diào)函數(shù)傳遞給指令來實(shí)現(xiàn):
app.directive('pos', (el, binding) => {
el.style.top = binding.value + 'px'
})
如果應(yīng)用場景升級,我們不僅希望它只是是在 top
上的偏移,而是通過我們指定傳入的方向值進(jìn)行偏移,那么應(yīng)該如何實(shí)現(xiàn)呢?這時使用動態(tài)參數(shù)就可以非常方便地根據(jù)每個組件實(shí)例來進(jìn)行更新。
// 場景二:動態(tài)指令參數(shù)
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos:[direction]="100" />
`,
data() {
return {
direction: 'bottom'
}
}
})
app.directive('pos', {
mounted(el, binding) {
// binding.arg 是我們傳遞給指令的參數(shù)
const direction = binding.arg || 'top'
el.style[direction] = binding.value + 'px'
}
})
你可以試著使用自定義組件完成一個這樣的功能?
插件
我們在 vue
項(xiàng)目中經(jīng)常會使用別人寫好的插件,例如 vue-router
、vue-touch
等,那么我們?nèi)绾巫约壕帉懸粋€插件呢?看官網(wǎng)的介紹:插件是自包含的代碼,通常向 Vue
添加全局級功能。它可以是公開 install()
方法的 object
,也可以是 function
。光看這句話可能有點(diǎn)懵,其實(shí)就傳達(dá)給了我們兩點(diǎn)訊息:
1、編寫插件可以是一個對象寫法,也可以是一個函數(shù)寫法
2、插件有一個公開的install()
默認(rèn)方法,它接收 vue 實(shí)例和你自定義的屬性兩個形參。
舉個栗子:
// 對象寫法:
const myPlugin = {
install(app, options) {
console.log(app, options) // vue 實(shí)例,{name: "cc"}
}
}
app.use(myPlugin, { name: 'cc' })
//函數(shù)寫法:
const myPlugin = (app, options) => {
console.log(app, options)
}
app.use(myPlugin, { name: 'cc' })
插件一般怎么寫呢?我們使用插件的時候額外的參數(shù)會放到 options
中,而 app
是使用這個插件的時候 vue
對應(yīng)的實(shí)例。我們既然能得到實(shí)例,我們就可以對其做很多拓展,例如:
const app = Vue.createApp({
template: `
<child></child>
`
})
// 子組件就可以通過 inject 接收到我們寫的插件里的 `name`
app.component('child', {
inject: ['name'],
template: `<div>{{name}}</div>`
})
// 自己寫插件,在上面通過 `provide` 拓展一個 name 屬性
const myPlugin = (app, options) => {
app.provide('name', 'cc')
}
app.use(myPlugin, { name: 'cc' })
官網(wǎng)栗子中給出了 app.config.globalProperties
這個語法,其實(shí)就是對 vue
全局的屬性做一些拓展,比如我們想在全局上加入 sayHello
這樣一個屬性,我們一般會使用 app.config.globalProperties.$sayHello
這樣去寫,在屬性名前加入 $
符號代表這是我們自己在 vue
全局添加的一個私有屬性,更方便我們管理。此時我們就可以在組件中直接訪問到這個全局私有屬性:
app.config.globalProperties.$sayHello = 'hello cc'
// 子組件直接通過 this 使用 $sayHello 屬性
app.component('child', {
inject: ['name'],
template: `<div>{{name}}</div>`,
mounted() {
console.log(this.$sayHello)
}
})
結(jié)合官網(wǎng),我們是否可以簡單的寫一個小插件,例如表單中的 input
框輸入檢測,對輸入的值進(jìn)行一些基礎(chǔ)的校驗(yàn),如下栗子:
當(dāng)然,這個簡單的小栗子肯定難不倒聰明的我們,其實(shí)我們可以使用一個全局 mixin
就可以完成這個功能:
const app = Vue.createApp({
data() {
return {
name: 'cc',
age: '18'
}
},
template: `
<div>
姓名: <input type="text" v-model="name" />
<span class="hint" v-if="this.$options.rules.name.error">
{{this.$options.rules.name.message}}
</span>
</div>
<div>
年齡: <input type="number" v-model="age" />
<span class="hint" v-if="this.$options.rules.age.error">
{{this.$options.rules.age.message}}
</span>
</div>
`,
rules: {
name: {
validate: name => name.length > 3,
error: false,
message: '用戶名最少為4個字符'
},
age: {
validate: age => age > 20,
error: false,
message: '年齡不能小于 20 歲'
}
}
})
// 校驗(yàn)插件
const validatorPlugin = (app, options) => {
app.mixin({
created() {
const rules = this.$options.rules
for (let key in rules) {
let item = rules[key]
this.$watch(key, (value) => {
if (!item.validate(value)) {
item.error = true
} else {
item.error = false
}
}, {
immediate: true
})
}
}
})
}
app.use(validatorPlugin)
我們在組件中定義了 rules
屬性,所以我們可以通過 this.$options.rules
直接訪問到這個屬性,然后我們通過 watch
監(jiān)聽 name
和 age
的變化,通過回調(diào)函數(shù)來校驗(yàn)值是否滿足條件,當(dāng)然判斷的過程中我們知道 watch
是有惰性的,所以我們在 watch
的配置中要加上 immediate: true
,這樣就可以在頁面加載完成時立即執(zhí)行。這樣我們就完成了一個迷你版的 input
校驗(yàn)功能。
自定義 v-model
vue2
中自定義 v-model
的實(shí)現(xiàn)及雙向綁定的原理我已經(jīng)寫過對應(yīng)的文章了,Vue 中如何自定義 v-model 及雙向綁定的實(shí)現(xiàn)原理 ,老版本的 v-model
有幾個痛點(diǎn):
1、比較繁瑣,要添加一個 model 屬性
2、組件上只能有一個 v-model,如果組件上出現(xiàn)多個 v-model,實(shí)現(xiàn)就比較困難
3、對初學(xué)者比較不友好,看的云里霧里
所以在 vue3
中對 v-model
也是進(jìn)行了大刀闊斧的改革,在 vue3
中實(shí)現(xiàn) v-model
不需要再給組件添加一個 model
屬性,只需要:
1、在組件的
props
中添加一個modelValue
的屬性
2、更新值的時候組件中的emit
時有一個update:modelValue
的方法
我們直接通過一個栗子來認(rèn)識 vue3
中的自定義 v-model
:
const app = Vue.createApp({
data() {
return {
count: 1
}
},
template: `
<div>{{count}}</div>
<child v-model="count"></child>
`
})
app.component('child', {
// props 中默認(rèn)的 modelValue 接收父組件中 count 的值
props: {
modelValue: String
},
template: `
<div @click="handleClick">{{modelValue}}</div>
`,
methods: {
handleClick() {
// 組件更新值的時候使用規(guī)定的 `update: modelValue` 方法
this.$emit('update:modelValue', this.modelValue + 3)
}
}
})
前面說過,vue3
中可以使用多個 v-model
,那么我們在來看看多個 v-model
的應(yīng)用:
const app = Vue.createApp({
data() {
return {
count: 1,
age: 18
}
},
template: `
<div>父組件的值</div>
<div>{{count}}</div>
<div>{{age}}</div>
<div>子組件 v-model 綁定的值</div>
<child v-model="count" v-model:age="age"></child>
`
})
app.component('child', {
props: {
modelValue: Number,
age: Number
},
template: `
<div @click="handleClick">{{modelValue}}</div>
<input :value="age" @input="handleInput" type="number" />
`,
methods: {
handleClick() {
this.$emit('update:modelValue', this.modelValue + 3)
},
handleInput(event) {
this.$emit('update:age', +(event.target.value))
}
}
})
當(dāng)我們在 v-model
后面不接入任何參數(shù)時,就可以直接在子組件中使用默認(rèn)的 modelValue
與父組件中 v-model
的值進(jìn)行綁定,而當(dāng)我們在 v-model:age
傳入 age
參數(shù)之后,對應(yīng)的子組件的 props
中也需要改成 age
,而更新值的時候組件中的 emit
中的方法也要改成對應(yīng)的 update: age
。其實(shí)新版本中的 v-model
使用更簡單更方便,同時可以綁定多個互不干擾。
非 Prop 的 Attribute
官網(wǎng)給出的定義為一個非 prop
的 attribute
是指傳向一個組件,但是該組件并沒有相應(yīng) props 或 emits 定義的 attribute
。常見的示例包括 class
、style
和 id
屬性。咋看這段解釋可能有點(diǎn)懵,其實(shí)我們可以通過一些栗子來看問題
Attribute 繼承
當(dāng)組件返回單個根節(jié)點(diǎn)時,非 prop attribute
將自動添加到根節(jié)點(diǎn)的 attribute
中。例如下列栗子:
const app = Vue.createApp({
template: `
<child type="number" class="parent"></child>
`
})
app.component('child', {
template: `
<div class="child">
<input />
</div>
`
})
被渲染的 child
組件實(shí)際代碼結(jié)構(gòu)如下:
// class 和 type 都被渲染到根節(jié)點(diǎn)上去了
<div class="child parent" type="number">
<input>
</div>
禁用 Attribute 繼承
如果你不希望組件的根元素繼承 attribute
,你可以在組件的選項(xiàng)中設(shè)置 inheritAttrs: false
。例如:禁用 attribute
繼承的常見情況是需要將 attribute
應(yīng)用于根節(jié)點(diǎn)之外的其他元素。
通過將 inheritAttrs
選項(xiàng)設(shè)置為 false
,你可以訪問組件的 $attrs property
,該 property
包括組件 props
和 emits property
中未包含的所有屬性 (例如,class
、style
、v-on
監(jiān)聽器等)。還是上面的栗子,我們需要 child
組件中的 input
去渲染對應(yīng)的 class
和 type
,我們就可以將代碼改寫一下:
app.component('child', {
inheritAttrs: false,
template: `
<div class="child">
<input v-bind="$attrs" />
</div>
`
})
此時我們再從瀏覽器中查看 DOM
元素節(jié)點(diǎn)就可以看到如下結(jié)構(gòu):
<div class="child">
<input type="number" class="parent">
</div>
多個根節(jié)點(diǎn)上的 Attribute 繼承
如果我們的子組件存在多個根節(jié)點(diǎn)怎么辦,例如:
const app = Vue.createApp({
template: `
<child class="child"></child>
`
})
app.component('child', {
template: `
<div class="header" >
<input />
</div>
<div class="main" v-bind="$attrs">main</div>
<div class="footer">footer</div>
`
})
如果我們不在其中一個根組件使用 v-bind = "$attrs"
控制臺就會給我們報(bào)錯,我們在其中一個根節(jié)點(diǎn)上使用之后父組件上對應(yīng)的 attribute
就會被繼承到這個根組件上。
當(dāng)然我們也可以禁止掉根節(jié)點(diǎn)上的繼承,直接在 header
結(jié)構(gòu)下的 input
框加入 v-bind = "$attrs"
即可。如下栗子:
app.component('child', {
inheritAttrs: false,
template: `
<div class="header" >
<input v-bind="$attrs" />
</div>
<div class="main">main</div>
<div class="footer">footer</div>
`
})
查漏補(bǔ)缺
我們知道在 vue2
模板中可以通過在 DOM
結(jié)構(gòu)中指定 ref
屬性,然后在邏輯代碼中通過 this.$refs.
去操作 DOM
,那么在 vue3
中我們應(yīng)該如何操作 DOM
元素呢?
我們先來一個簡單的場景,判斷點(diǎn)擊的 dom
元素是否在 id = 'index'
的 dom
結(jié)構(gòu)中,場景代碼如下:
// 判斷點(diǎn)擊的 dom 元素節(jié)點(diǎn)是不是在 index 中
const app = Vue.createApp({
template: `
<div id="index">
<div id="index-list">555</div>
</div>
<div id="demo">666</div>
`,
})
此時我們就要進(jìn)行 DOM
元素節(jié)點(diǎn)判斷,結(jié)合 setup
我們應(yīng)該如何去使用 ref
來獲取 dom
元素節(jié)點(diǎn)呢?代碼比較簡單就直接上結(jié)果了:
const app = Vue.createApp({
template: `
<div id="index" ref="index">
<div id="index-list">555</div>
</div>
<div id="demo">666</div>
`,
setup() {
const { ref, onMounted } = Vue
const index = ref(null)
onMounted(() => {
document.addEventListener('click', function (e) {
if (index.value.contains(e.target)) {
console.log('點(diǎn)擊的是 index 里面的元素')
} else {
console.log('點(diǎn)擊的不是 index 里面的元素')
}
})
})
return { index }
}
})
因?yàn)?setup
的執(zhí)行是在 beforeCreate
和 created
之間,所以我們?nèi)绻肽玫綄?yīng)的 dom
元素節(jié)點(diǎn),最好在其內(nèi)部的生命周期中進(jìn)行獲取。
vue3
中相較 vue2
大體的改動和日常開發(fā)中經(jīng)常會遇到的問題基本都已經(jīng)整理的差不多了,由于 vue3
代碼基本都是 ts
寫的,所以學(xué)習(xí) ts
其實(shí)已經(jīng)迫在眉睫的。結(jié)尾綜合做個小栗子吧:
其實(shí)很簡單,就是一個 form 表單提交驗(yàn)證,不過封裝基本用的是 vue3 + ts
,小伙伴可以自己獨(dú)立實(shí)現(xiàn)一個類似 element-ui
中的表單效驗(yàn)插件,其實(shí)組件開發(fā)更多的是學(xué)習(xí)思路以及代碼的擴(kuò)展性,優(yōu)雅性。最近 github
經(jīng)常打不開,源代碼就放在 gitee 上了。本文多為自己學(xué)習(xí)筆記記錄,如有錯誤,歡迎指正!!!