組件注冊
分為全局注冊和局部注冊兩種。
全局注冊:
在main.js或main.ts中,使用 Vue 應用實例的 .component() 方法,讓組件在當前 Vue 應用中全局可用。
import { createApp } from 'vue'
import MyComponent from './App.vue'const app = createApp({})app.component('MyComponent', MyComponent)
.component() 方法可以被鏈式調用:
app.component('ComponentA', ComponentA).component('ComponentB', ComponentB).component('ComponentC', ComponentC)
全局注冊后,組件可以在此應用的任意組件的模板中使用:
<!-- 這在當前應用的任意組件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
全局注冊存在的問題:
- 全局注冊,但并沒有被使用的組件無法在生產打包時被自動移除 (也叫“tree-shaking”)。如果你全局注冊了一個組件,即使它并沒有被實際使用,它仍然會出現在打包后的 JS 文件中。
- 全局注冊在大型項目中使項目的依賴關系變得不那么明確。在父組件中使用子組件時,不太容易定位子組件的實現。和使用過多的全局變量一樣,這可能會影響應用長期的可維護性。
局部組件:
一、在使用 <script setup> 的單文件組件中,導入的組件可以直接在模板中使用,無需注冊:
<script setup>
import ComponentA from './ComponentA.vue'
</script><template><ComponentA />
</template>
二、如果沒有使用 <script setup>,則需要使用 components 選項來顯式注冊:
import ComponentA from './ComponentA.js'export default {components: {ComponentA: ComponentA // 根據ES2015可直接縮寫成ComponentA},setup() {// ...}
}
Tips:
局部注冊的組件僅在當前組件中可用,在后代組件中不可用。
組件數據傳遞
props接收聲明
在使用 <script setup> 的單文件組件中,props 可以使用 defineProps() 宏(所有的宏都無需引入)來聲明:
<script setup>
const props = defineProps(['foo'])console.log(props.foo)
</script>
在沒有使用 <script setup> 的組件中,props 可以使用 props 選項來聲明:
export default {props: ['foo'],setup(props) {// setup() 接收 props 作為第一個參數console.log(props.foo)}
}
除了使用字符串數組來聲明 prop 外,還可以使用對象的形式,并聲明prop項的類型:
// 對于以對象形式聲明的每個屬性,key 是 prop 的名稱,而值則是該 prop 預期類型的構造函數。
// 使用 <script setup>
defineProps({title: [String, Number], // 字符串或數字類型likes: Number
})
// 非 <script setup>
export default {props: {title: [String, Number],likes: Number}
}// 在ts中也可以使用類型標注來聲明
<script setup lang="ts">defineProps<{title?: stringlikes?: number}>()
</script>
Prop 名字格式
為了和 HTML attribute 對齊,我們通常在向子組件傳遞 props時,會將其寫為 kebab-case 形式:
<MyComponent greeting-message="hello" />
還可以傳入動態值:
<!-- 根據一個變量的值動態傳入 -->
<BlogPost :likes="post.likes" /><!-- 僅寫上 prop 但不傳值,會隱式轉換為 `true` -->
<BlogPost is-published />
使用一個對象綁定多個 prop
如果你想要將一個對象的所有屬性都當作 props 傳入,你可以使用沒有參數的 v-bind,即只使用 v-bind 而非 :prop-name。如下所示:
const post = {id: 1,title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
// 等價于
<BlogPost :id="post.id" :title="post.title" />
單向數據流
所有的 props 都遵循著單向綁定原則,props 因父組件的更新而變化,自然地將新的狀態向下流往子組件,而不會逆向傳遞。這避免了子組件意外修改父組件的狀態的情況,不然應用的數據流將很容易變得混亂而難以理解。如果強行在子組件上修改props,Vue 會在控制臺上向你拋出警告。
如果實在有修改的需求,可以用prop作為初始值后,新定義一個局部數據屬性:
const props = defineProps(['initialCounter'])
// 計數器只是將 props.initialCounter 作為初始值
// 像下面這樣做就使 prop 和后續更新無關了
const counter = ref(props.initialCounter)
后子組件通過拋出事件defineEmits,來通知父組件同步更改的傳入的prop。
Prop校驗
寫了類型的聲明時,如果傳入的值不滿足類型要求,Vue 會在瀏覽器控制臺中拋出警告來提醒使用者。這在開發給其他開發者使用的組件時非常有用。
聲明對 props 的校驗有如下方法:
defineProps({// 基礎類型檢查// (給出 `null` 和 `undefined` 值則會跳過任何類型檢查)propA: Number,// 多種可能的類型propB: [String, Number],// 必傳,且為 String 類型propC: {type: String,required: true},// 必傳但可為空的字符串propD: {type: [String, null],required: true},// Number 類型的默認值propE: {type: Number,default: 100},// 對象類型的默認值propF: {type: Object,// 對象或數組的默認值// 必須從一個工廠函數返回。// 該函數接收組件所接收到的原始 prop 作為參數。default(rawProps) {return { message: 'hello' }}// 可簡寫為default: () => ({ message: 'hello' })},// 自定義類型校驗函數// 在 3.4+ 中完整的 props 作為第二個參數傳入propG: {validator(value, props) {// The value must match one of these stringsreturn ['success', 'warning', 'danger'].includes(value)}},// 函數類型的默認值propH: {type: Function,// 不像對象或數組的默認,這不是一個// 工廠函數。這會是一個用來作為默認值的函數default() {return 'Default function'}}
})
vue3+TS時,語法如下:
export interface Props {msg?: stringlabels?: string[]
}const props = withDefaults(defineProps<Props>(), {msg: 'hello',labels: () => ['one', 'two']
})
Tips:
- defineProps() 宏中的參數不可以訪問 <script setup> 中定義的其他變量,因為在編譯時整個表達式都會被移到外部的函數中。
- 所有 prop 默認都是可選的,除非聲明了 required: true。
- 除 Boolean 外的未傳遞的可選 prop 將會有一個默認值 undefined。
- Boolean 類型的未傳遞 prop 將被轉換為 false。這可以通過為它設置 default 來更改——例如:設置為 default: undefined 將與非布爾類型的 prop 的行為保持一致。
- 如果聲明了 default 值,那么在 prop 的值被解析為 undefined 時,無論 prop 是未被傳遞還是顯式指明的 undefined,都會改為 default 值。
運行時類型檢查
Vue 主要通過 instanceof 來檢查類型是否匹配。
如果 type 僅為 null 而非使用數組語法,它將允許任何類型。
校驗中的type可以是哪些:
1、構造函數
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
- Error
2、type 也可以是自定義的類或構造函數
例子:
class Person {constructor(firstName, lastName) {this.firstName = firstNamethis.lastName = lastName}
}
defineProps({author: Person
})
Boolean 類型轉換
聲明為 Boolean 類型的 props 有特別的類型轉換規則。
例子:
defineProps({disabled: Boolean
})
// 應用
<!-- 等同于傳入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于傳入 :disabled="false" -->
<MyComponent />
當prop被聲明為多種類型,并且類型中有String時,Boolean放置的位置會影響Boolean 的轉換規則應用。
// disabled 將被轉換為 true
defineProps({disabled: [Boolean, Number]
})// disabled 將被轉換為 true
defineProps({disabled: [Boolean, String]
})// disabled 將被轉換為 true
defineProps({disabled: [Number, Boolean]
})// disabled 將被解析為空字符串 (disabled="")
defineProps({disabled: [String, Boolean]
})
組件事件
觸發與監聽事件
子組件在模板表達式中,可以直接使用 $emit 方法觸發自定義事件:
<!-- MyComponent -->
<button @click="$emit('someEvent')">Click Me</button>
父組件可以通過 v-on (縮寫為 @) 來監聽事件(組件的事件監聽器也支持 .once 修飾符):
<MyComponent @some-event="callback" />
Tips:
和原生 DOM 事件不一樣,組件觸發的事件沒有冒泡機制。你只能監聽直接子組件觸發的事件。平級組件或是跨越多層嵌套的組件間通信,應使用一個外部的事件總線,或是使用一個狀態管理 | Vue.js。
帶參數事件
有時候我們會需要在觸發事件時附帶一個特定的值。
子組件中使用$emit 方法觸發自定義事件,提供一個額外的參數:
<button @click="$emit('increaseBy', 1)">Increase by 1
</button>
父組件接收:
// 方法一
<MyButton @increase-by="(n) => count += n" />// 方法二
<MyButton @increase-by="increaseCount" />
function increaseCount(n) {count.value += n
}
Tips:
所有傳入 $emit() 的額外參數都會被直接傳向監聽器。舉例來說,$emit('foo', 1, 2, 3) 觸發后,監聽器函數將會收到這三個參數值。
用defineEmits()聲明觸發的事件
組件可以顯式地通過 defineEmits() 宏來聲明它要觸發的事件:
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
在 <template> 中使用的 $emit 方法不能在組件的 <script setup> 部分中使用,但 defineEmits() 會返回一個相同作用的函數供我們使用:
<script setup>
const emit = defineEmits(['inFocus', 'submit'])function buttonClick() {emit('submit')
}
</script>
defineEmits() 宏不能在子函數中使用。如上所示,它必須直接放置在 <script setup> 的頂級作用域下。
使用選項式setup的寫法:
export default {emits: ['inFocus', 'submit'],setup(props, ctx) {ctx.emit('submit')}
}
// ctx支持解構
export default {emits: ['inFocus', 'submit'],setup(props, { emit }) {emit('submit')}
}
emits 選項和 defineEmits() 宏還支持對象語法。通過 TypeScript 為參數指定類型,它允許我們對觸發事件的參數進行驗證:
<script setup lang="ts">
const emit = defineEmits({submit(payload: { email: string, password: string }) {// 通過返回值為 `true` 還是為 `false` 來判斷驗證是否通過}
})
</script>
完整版:
<script setup lang="ts">
const emit = defineEmits({// 沒有校驗click: null,// 校驗 submit 事件submit: (payload: { email: string, password: string }) => {if (email && password) {return true} else {console.warn('Invalid submit event payload!')return false}}
})function submitForm(email: string, password: string) {emit('submit', { email, password })
}
</script>
也可以使用純類型標注來聲明觸發的事件:
<script setup lang="ts">
const emit = defineEmits<{(e: 'change', id: number): void(e: 'update', value: string): void
}>()// 3.3+: 可選的、更簡潔的語法
const emit = defineEmits<{change: [id: number]update: [value: string]
}>()
</script>
Tips:
如果一個原生事件的名字 (例如 click) 被定義在 emits 選項中,則監聽器只會監聽組件觸發的 click 事件而不會再響應原生的 click 事件。
組件v-model
基本用法
v-model 可以在組件上使用以實現雙向綁定。
從 Vue 3.4 開始,推薦的實現方式是使用 defineModel() 宏:
<!-- Parent.vue -->
<Child v-model="countModel" /><!-- Child.vue -->
<script setup>
const model = defineModel()function update() {model.value++
}
</script><template><div>Parent bound v-model is: {{ model }}</div>
</template>
defineModel() 返回的值是一個 ref。它可以像其他 ref 一樣被訪問以及修改,不過它能起到在父組件和當前變量之間的雙向綁定的作用:
- 它的 .value 和父組件的 v-model 的值同步;
- 當它被子組件變更了,會觸發父組件綁定的值一起更新。
底層機制:
defineModel 是一個便利宏。編譯器將其展開為以下內容:
- 一個名為 modelValue 的 prop,本地 ref 的值與其同步;
- 一個名為 update:modelValue 的事件,當本地 ref 的值發生變更時觸發。
defineModel 的實現原理如下:
子組件
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script><template><input:value="props.modelValue"@input="emit('update:modelValue', $event.target.value)"/>
</template>
父組件
<!-- Parent.vue -->
<Child:modelValue="foo"@update:modelValue="$event => (foo = $event)"
/>
未使用defineModel 的代碼顯得十分冗長,使用defineModel 后可簡潔如下:
子組件
<!-- Child.vue -->
<script setup>
const model = defineModel()
</script><template><span>My input</span> <input v-model="model">
</template>
父組件
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'const msg = ref('Hello World!')
</script><template><h1>{{ msg }}</h1><Child v-model="msg" />
</template>
因為 defineModel 相當于在底層聲明了一個 prop,你可以通過給 defineModel 傳遞選項,來聲明底層 prop 的選項:
// 使 v-model 必填
const model = defineModel({ required: true })// 提供一個默認值
const model = defineModel({ default: 0 })
注意:
如果為 defineModel prop 設置了一個 default 值且父組件沒有為該 prop 提供任何值,會導致父組件與子組件之間不同步。在下面的示例中,父組件的 myRef 是 undefined,而子組件的 model 是 1:
// 子組件: const model = defineModel({ default: 1 })// 父組件 const myRef = ref() <Child v-model="myRef"></Child>
v-model 的參數
組件上的 v-model 也可以接受一個參數:
<MyComponent v-model:title="bookTitle" />
在子組件中,我們可以通過將字符串作為第一個參數傳遞給 defineModel() 來支持相應的參數:
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script><template><input type="text" v-model="title" />
</template>
如果需要額外的 prop 選項,應該在 model 名稱之后傳遞:
const title = defineModel('title', { required: true })
3.4 之前的用法
<!-- MyComponent.vue -->
<script setup>
defineProps({title: {required: true}
})
defineEmits(['update:title'])
</script><template><inputtype="text":value="title"@input="$emit('update:title', $event.target.value)"/>
</template>
多個v-model的使用方法
<!-- Parent.vue -->
<UserNamev-model:first-name="first"v-model:last-name="last"
/>
<!-- Child.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script><template><input type="text" v-model="firstName" /><input type="text" v-model="lastName" />
</template>
3.4 之前的用法
<script setup>
defineProps({firstName: String,lastName: String
})defineEmits(['update:firstName', 'update:lastName'])
</script><template><inputtype="text":value="firstName"@input="$emit('update:firstName', $event.target.value)"/><inputtype="text":value="lastName"@input="$emit('update:lastName', $event.target.value)"/>
</template>
處理 v-model 修飾符
如下所示,capitalize(將輸入的字符串值第一個字母轉為大寫)為自定義的修飾符:
<MyComponent v-model.capitalize="myText" />
解構?defineModel() 的返回值,可以在子組件中訪問添加到組件 v-model 的修飾符:
<script setup>
const [model, modifiers] = defineModel()console.log(modifiers) // { capitalize: true }
</script><template><input type="text" v-model="model" />
</template>
?defineModel() 中傳入 get 和 set 這兩個選項,在從模型引用中讀取或設置值時會接收到當前的值,并且它們都應該返回一個經過處理的新值。下面是一個例子,展示了如何利用 set 選項來應用 capitalize (首字母大寫) 修飾符:
<script setup>
const [model, modifiers] = defineModel({set(value) {if (modifiers.capitalize) {return value.charAt(0).toUpperCase() + value.slice(1)}return value}
})
</script><template><input type="text" v-model="model" />
</template>
3.4 之前的用法
<script setup>
const props = defineProps({modelValue: String,modelModifiers: { default: () => ({}) }
})const emit = defineEmits(['update:modelValue'])function emitValue(e) {let value = e.target.valueif (props.modelModifiers.capitalize) {value = value.charAt(0).toUpperCase() + value.slice(1)}emit('update:modelValue', value)
}
</script><template><input type="text" :value="modelValue" @input="emitValue" />
</template>
使用多個不同參數的 v-model 時使用修飾符:
<!-- Parent.vue -->
<UserNamev-model:first-name.capitalize="first"v-model:last-name.uppercase="last"
/><!-- Child.vue -->
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
透傳 Attributes
Attributes 繼承
“透傳 attribute”指的是傳遞給一個組件,卻沒有被該組件聲明為 props 或 emits 的 attribute 或者 v-on 事件監聽器。最常見的例子就是 class、style 和 id及其他傳遞了但沒有聲明使用的prop。
如下例子,一個組件以單個元素為根作渲染時,透傳的 attribute 會自動被添加到根元素上。
<!-- <MyButton> -->
<button>Click Me</button><!-- Parent.vue -->
<MyButton class="large" /><!-- 最后渲染出的 DOM 結果 -->
<!-- 父組件中的class透傳到了MyButton組件的根元素button 上 -->
<button class="large">Click Me</button>
對 class 和 style 的合并
如果一個子組件的根元素已經有了 class 或 style attribute,它會和從父組件上繼承的值合并。
<!-- <MyButton> -->
<button class="btn">Click Me</button><!-- Parent.vue -->
<MyButton class="large" /><!-- 最后渲染出的 DOM 結果 -->
<button class="large btn">Click Me</button>
v-on 監聽器繼承
規則同上,而且事件也會合并。如果原生 button 元素自身也通過 v-on 綁定了一個事件監聽器,則這個監聽器和從父組件繼承的監聽器都會被觸發。
深層組件繼承
如果一個組件 (甲) 的根節點是另一個組件 (乙) ,甲組件接收的透傳 attribute 會直接繼續傳給乙組件。
注意:
- 透傳的 attribute 不會包含?甲組件?上聲明過的 props 或是針對 emits 聲明事件的 v-on 偵聽函數,換句話說,聲明過的 props 和偵聽函數被 甲組件?“消費”了。
- 透傳的 attribute 若符合聲明,也可以作為 props 傳入 乙組件。
禁用 Attributes 繼承
如果要禁止一個組件自動繼承attribute,可以在組件選項中設置 inheritAttrs: false。
Vue3.3+的使用方法,直接在 <script setup> 中使用 defineOptions:
<script setup>
defineOptions({inheritAttrs: false
})
// ...setup 邏輯
</script>
最常見的需要禁用 attribute 繼承的場景就是 attribute 需要應用在根節點以外的其他元素上。通過設置 inheritAttrs 選項為 false,你可以完全控制透傳進來的 attribute 被如何使用。
這些透傳進來的 attribute 可以在模板的表達式中直接用 $attrs 訪問到。
<span>Fallthrough attribute: {{ $attrs }}</span>
注意:
- 和 props 有所不同,透傳 attributes 在 JavaScript 中保留了它們原始的大小寫,所以像 foo-bar 這樣的一個 attribute 需要通過 $attrs['foo-bar'] 來訪問。
- 像 @click 這樣的一個 v-on 事件監聽器將在此對象下被暴露為一個函數 $attrs.onClick。
如下例,如果我們要想在 <MyButton> 中的button接收透傳進來的 attribute,而不是外層div上
<!-- <MyButton> -->
<div class="btn-wrapper"><button class="btn">Click Me</button>
</div><!-- Parent.vue -->
<MyButton class="large" />
則需要將?<MyButton> 改為:
<script setup>
defineOptions({inheritAttrs: false
})
</script><div class="btn-wrapper"><button class="btn" v-bind="$attrs">Click Me</button>
</div>
多根節點的 Attributes 繼承
和單根節點組件有所不同,有著多個根節點的組件沒有自動 attribute 透傳行為。如果 $attrs 沒有被顯式綁定,將會拋出一個運行時警告。
如下例中,子組件中因為有多個根節點,并且沒有顯式綁定$attrs,控制臺就會拋出一個警告。
<!-- Parent.vue -->
<CustomLayout id="custom-layout" @click="changeValue" /><!-- Child.vue -->
<header>...</header>
<main>...</main>
<footer>...</footer>
需要在子組件中綁定$attrs,警告就會消失:
<!-- Child.vue -->
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
在 JavaScript 中訪問透傳 Attributes
如果需要,你可以在 <script setup> 中使用 useAttrs() API 來訪問一個組件的所有透傳 attribute:
<script setup>
import { useAttrs } from 'vue'const attrs = useAttrs()
</script>
如果使用的setup選項:
export default {setup(props, ctx) {// 透傳 attribute 被暴露為 ctx.attrsconsole.log(ctx.attrs)}
}
注意:
????????雖然這里的 attrs 對象總是反映為最新的透傳 attribute,但它并不是響應式的 (考慮到性能因素)。你不能通過偵聽器去監聽它的變化。如果你需要響應性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新時結合最新的 attrs 執行副作用。