vue3 快速入門系列 —— 元件通訊

彭加李發表於2024-04-17

其他章節請看:

vue3 快速入門 系列

元件通訊

元件通訊在開發中非常重要,通訊就是你給我一點東西,我給你一點東西。

本篇將分析 vue3 中元件間的通訊方式。

Tip:下文提到的絕大多數通訊方式在 vue2 中都有,但是在寫法上有一些差異。

準備環境

vue3 基礎上進行。

新建三個元件:爺爺、父親、孩子A、孩子B,在主頁 Home.vue 中載入元件Gradfather.vue

<!-- Gradfather.vue -->
<template>
    <p># 爺爺</p>
    <hr>
    <Father/>
</template>

<script lang="ts" setup name="App">
import Father from './Father.vue';
</script>

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    <ChildA/>
    <hr>
    <ChildB/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
import ChildB from '@/views/ChildB.vue'
</script>
<!-- ChildA.vue -->
<template>
    <p># 孩子A</p>
</template>

<script lang="ts" setup name="App">

</script>
<!-- ChildB.vue -->
<template>
    <p># 孩子B</p>
</template>

<script lang="ts" setup name="App">

</script>

瀏覽器呈現:

# 爺爺
——————————————————
# 父親
——————————————————
# 孩子A
——————————————————
# 孩子B

下文將再此基礎上演示元件間的通訊。

props

需求:實現父給子一件新衣服,子給父一個吻,都用 props 實現。

請看程式碼:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>來自孩子A: {{ b }}</p>
    <hr>
    // 傳一個屬性和一個方法
    <ChildA :gift="a" :sendWen="getWen"/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import {ref} from 'vue'
let a = ref('新衣服')

let b = ref('')

function getWen(val:string){
    b.value = val
}
</script>
<!-- ChildA.vue -->
<template>
    <p># 孩子A</p>
    <p>來自父親:{{ gift }}</p>
</template>

<script lang="ts" setup name="App">
const props = defineProps(['gift', 'sendWen'])
// 呼叫方法,透過引數傳遞資料給父元件
props.sendWen('kiss')
</script>

頁面呈現:

# 父親

來自孩子A: kiss
————————————————————
# 孩子A

來自父親:新衣服

子給父傳資料藉助了方法。

通常我們可能會用自定義事件來向父元件傳遞資料,但是在 react 中,子元件給父元件傳遞資料就是用 props 傳遞方法的這種方式進行的。

Tip:祖父給孫子傳遞就不要用 props。否則按照這個思路,無論什麼情況都可以用這個方法。

自定義事件

請看示例:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>來自孩子A: {{ b }}</p>
    <hr>
    <!-- send-gift 肉串命名,一個單詞就像一塊肉 kebab-case。官方推薦 -->
    <ChildA :gift="a" @send-gift="getGift"/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import {ref} from 'vue'
let a = ref('新衣服2')

let b = ref('')

function getGift(val:string){
    b.value = val
}
</script>

父元件透過 @send-gift="getGift" 給孩子繫結自定義事件,子元件透過 defineEmits 宣告可以觸發的事件,最後透過 emit('send-gift', 'kiss2') 觸發事件,並將引數傳過去。

<!-- ChildA.vue -->
<template>
    <p># 孩子A</p>
    <p>來自父親:{{ gift }}</p>
</template>

<script lang="ts" setup name="App">
defineProps(['gift',])
// 宣告事件 - 定義一個元件可以發射(emit)的事件
const emit = defineEmits(['send-gift'])
emit('send-gift', 'kiss2')

</script>

瀏覽器呈現:

# 爺爺
——————————————————
# 父親

來自孩子A: kiss2
——————————————————
# 孩子A

來自父親:新衣服2

Tip:我們推薦你始終使用 kebab-case 的事件名 —— vue2 官網 - 事件名

mitt

在 vue2 中我們學過中央事件匯流排

Vue 3中,中央事件匯流排(Vue 2中的emit/on機制)已被廢除。Vue 3更加推崇使用組合 API、provide/inject以及props/emits來進行元件之間的通訊。這樣的做法使得元件通訊更加明確和可追蹤,並且更容易維護和理解。而像mitt這樣的第三方庫可以作為替代方案,用於實現更靈活的事件管理。

mitt 可以實現任意元件之間的通訊。

pubsub(例如 pubsub-js 庫)、$bus(例如 vue2 中的中央事件匯流排)、mitt 都是前端中常見的用於實現事件匯流排(Event Bus)或事件訂閱-釋出(Publish-Subscribe)模式的解決方案。這三者都是一個套路。也就是:

  • 接收資料:提前繫結(訂閱資料)
  • 提供資料:適時觸發(釋出訊息)

mitt 用法很簡單,直接看 mitt 倉庫。首先下載包:

PS hello_vue3>  npm install --save mitt

added 1 package, and audited 72 packages in 2s

10 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.
"mitt": "^3.0.1"

建立 emitt 並在 main.ts 將其引入專案:

// src\utils\emitter.ts
import mitt from 'mitt'

const emitter = mitt()

export default emitter
// 引入
import emitter from './utils/emitter'

需求:現在我們讓 ChildA 給 ChildB 送禮物。

請看實現:

ChildA 中觸發事件:emitter.emit

<!-- ChildA.vue -->
<template>
    <p># 孩子A</p>
    <button @click="emitter.emit('send-toy', '籃球')">給兄弟禮物</button>
</template>

<script lang="ts" setup name="App">
import emitter from '@/utils/emitter';
</script>

ChildB 中繫結事件:emitter.on

<!-- ChildB.vue -->
<template>
    <p># 孩子B</p>
    <p>兄弟送的禮物:{{ gift }}</p>
</template>

<script lang="ts" setup name="App">
import emitter from '@/utils/emitter';

import {ref} from 'vue'

let gift = ref('')

// 如果將 any 改成 string,vscode 報錯。暫時不知解決:
/*
沒有與此呼叫匹配的過載。
  第 1 個過載(共 2 個),“(type: "*", handler: WildcardHandler<Record<EventType, unknown>>): void”,出現以下錯誤。
  第 2 個過載(共 2 個),“(type: "get-toy", handler: Handler<unknown>): void”,出現以下錯誤。ts(2769)
*/
emitter.on('send-toy', (e: any) => {
  gift.value = e;
});

</script>

在 ChildA 中點選按鈕,B就能收到禮物。完成任意元件的通訊。

Tip: 建議元件解除安裝時解綁事件。就像這樣:

import {onUnmounted} from 'vue'

onUnmounted(() => {
    // 移除該型別的所有事件處理程式
    emitter.off('send-toy')
})

其他寫法有:

// 監聽
// foo { a: 'b' }
emitter.on('foo', e => console.log('foo', e) )
// 觸發
emitter.emit('foo', { a: 'b' })
// 監聽所有事件。比如 foo2 就會觸發
// foo2 {a: 'b'}
emitter.on('*', (type, e) => console.log(type, e) )
emitter.emit('foo2', { a: 'b' })
// 清除所有事件
emitter.all.clear()
// 註冊和解綁事件
function onFoo() {}
emitter.on('foo', onFoo)   // listen
emitter.off('foo', onFoo)  // unlisten

v-model

vue2 中 v-model 用於簡化父子之間的通訊

你可能不會經常直接在自定義元件中編寫 v-model,但是許多 UI 元件庫的底層確實會使用 v-model 來簡化父子元件之間的通訊和資料流動。這種設計可以使得使用這些元件時更加方便和直觀。

舉例來說,當你使用一個 UI 元件庫提供的輸入框元件時,通常可以透過 v-model 來實現父元件與該輸入框元件之間的雙向繫結,讓你可以直接在父元件中操作輸入框的值,而不需要手動監聽事件或者透過 props 和 emit 進行通訊。這種方式大大簡化了元件的使用方式和資料流動。

v-model 作用在 input 上可以實現雙向繫結,作用在元件上,也能實現父子元件之間的通訊(vue2 v-model數字輸入框元件

v-model 實際上是語法糖,對於 input,等於繫結了 :value 和 @input。就像這樣:

// vue2
<input v-model="message" placeholder="edit me"> 
等於
<input type="text" :value="message" @input="message = $event.target.value" placeholder="edit me">

vue3 中 v-model 類似,v-model 對應的是 modelValue 的 prop 和 update:modelValue 的事件。比如我想封裝一個 MyInput 元件。

<MyInput v-model="username"/>

等價

<MyInput 
    :modelValue="username"
    @update:modelValue="username = $event"
/>

需求:元件A使用 MyInput,透過 v-model 實現父子之間的通訊。

首先不用語法糖,實現如下:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>val: {{ val }}</p>
    // 方式1
    <MyInput :modelValue="val" @update:modelValue="changeVal"/>
</template>

<script lang="ts" setup name="App">
import MyInput from '@/views/MyInput.vue'
import {ref} from 'vue'
let val = ref('p')

function changeVal($event: string){
    val.value = $event
}
</script>

Tipupdate:modelValue 就是事件名,只是包含一個冒號。

<template>
    i am MyInput:
    <p><input :value="val" @input="handleInput" /></p>
</template>

<script lang="ts" setup name="App">
import { ref,toRefs } from 'vue'
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])

console.log('props: ', props);
// 元件接手初始值
// 注:之後父元件的 props 修改後,val 不會在響應,需要自己手動修改 val
let val = ref(props.modelValue)

function handleInput($event: Event){
    // 斷言是一個 input 物件。否則ts報錯:沒有 value
    val.value = (<HTMLInputElement>$event.target).value
    emits('update:modelValue', val.value)
}
</script>

瀏覽器呈現:

# 元件A

val: p

i am MyInput:

// 這是 input 元素
p

編輯 input 內容時, val 對應的值也會同步,於是實現了父子之間的通訊。

這三種方式在這裡完全可以替換,於是我們知道 v-model 確實就是個語法糖。

// 方式1
<MyInput :modelValue="val" @update:modelValue="changeVal"/>
// 方式2:模板自動對 ref 進行解包
<!-- <MyInput :modelValue="val" @update:modelValue="val = $event"/> -->
// 方式3
<!-- <MyInput v-model="val"/> -->

重新命名 modelValue

目前屬性名和方法名中預設是 modelValue,就像:<MyInput :modelValue="val" @update:modelValue="changeVal"/>,希望重新命名。

下面這個例子透過 v-model 同時傳2個值,並修改預設值。請看示例:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>val: {{ val }}</p>
    <p>val2: {{ val2 }}</p>
    <MyInput v-model:name="val" v-model:age="val2"/>
</template>

<script lang="ts" setup name="App">
import MyInput from '@/views/MyInput.vue'
import {ref} from 'vue'
let val = ref('p')
let val2 = ref(18)
</script>
<template>
    i am MyInput:
    <p><input :value="name" @input=" emits('update:name', (<HTMLInputElement>$event.target).value)" /></p>
    <p><input :value="age" @input=" emits('update:age', (<HTMLInputElement>$event.target).value)" /></p>
</template>

<script lang="ts" setup name="App">
const props = defineProps(['name', 'age'])
const emits = defineEmits(['update:name', 'update:age'])

</script>

$attrs

祖孫資料互傳可以使用 $attrs 實現。

Tip:$attrs 詳細請看:vue2 $attrs

父元件給子元件傳遞三個屬性,子元件透過 props 接收一個,剩餘2個屬性就會到 $attrs:

<!-- Gradfather.vue -->
<template>
    <p># 爺爺</p>
    <hr>
    <Father :name="name" :age="age" :tel="tel"/>
</template>

<script lang="ts" setup name="App">
let name = ref('peng')
let age = ref(18)
let tel = ref('131xxx')

// Vite 使用了 ES 模組的動態引入特性,允許在執行時動態載入模組,而不需要在編譯時就確定所有的依賴關係。
// 這種特性使得在 <script setup> 塊中將 import 放在尾部成為可能。
import Father from './Father.vue';
import {ref} from 'vue'
</script>
<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>$attrs {{ $attrs }}</p>
    <hr>
    <ChildA/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

defineProps(['name'])

</script>

接著用 $attrs 實現祖父給孫子傳遞資料。核心程式碼如下:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>$attrs {{ $attrs }}</p>
    <hr>
    <!-- v-bind 支援物件語法,這兩行是等價的 -->
    <ChildA v-bind="$attrs"/>
    <!-- <ChildA :name="name" :age="$attrs.age"/> -->
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>$attrs: {{ $attrs }}</p>
    <p>來自祖父的name: {{ name }}</p>
    <p>來自祖父的age: {{ age }}</p>
</template>

<script lang="ts" setup name="App">
defineProps(['name', 'age'])
</script>

瀏覽器呈現:

# 父親

$attrs { "name": "peng", "age": 18, "tel": "131xxx" }
————————————————————————————————————————————————————————
# 元件A

$attrs: { "tel": "131xxx" }

來自祖父的name: peng

來自祖父的age: 18

孫子給祖父傳資料,利用 props 的方法,這裡祖父提供一個修改電話的方法,孫子呼叫該方法即可。核心程式碼如下:

<!-- Gradfather.vue -->
<template>
    <p># 爺爺</p>
    <p>tel: {{ tel }}</p>
    <hr>
    <Father :name="name" :age="age" :tel="tel" :changeTel="changeTel"/>
</template>

<script lang="ts" setup name="App">
function changeTel(v: string){
    console.log('v: ', v);

    tel.value = v
}
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p><button @click="changeTel('132')">change 祖父 tel</button></p>
</template>

<script lang="ts" setup name="App">
defineProps(['name', 'age', 'changeTel'])
</script>

在孫子中點選按鈕,祖父的 tel 就會改變。

Tip:孫子給祖父傳遞資料也可以用自定義事件的升級版本 $listener。

$refs 和 $parent

Tip: 在Vue.js 2.x中,$refs是一個特殊的屬性,用於訪問元件或DOM元素的引用。當在模板中使用ref屬性給元素或元件命名時,Vue.js會自動生成一個$refs物件,其中包含了對這些元素或元件的引用。

需求:父給子一個玩具,子給父一個吻。

父元件透過 ref 給子元件一個玩具。請看程式碼:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p><button @click="sendGift">給孩子禮物</button></p>
    <hr>
    <ChildA ref="c1"/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import {ref} from 'vue'
const c1 = ref()

function sendGift(){
    // c1.value: Proxy(Object) {gift: RefImpl, __v_skip: true}
    console.log('c1.value: ', c1.value);
    c1.value.gift = '籃球'
}

</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>父親給的禮物:{{ gift }}</p>
</template>

<script lang="ts" setup name="App">

import {ref} from 'vue'
const gift = ref('')
// defineExpose 是一個用於在組合式 API 中將元件的屬性或方法暴露給父元件的函式
defineExpose({gift})
</script>

當在模板中使用ref屬性給元素或元件命名時,Vue.js會自動生成一個$refs物件。可以透過 $refs 給孩子禮物。請看示例:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p><button @click="sendGift">透過 ref 給孩子禮物</button></p>
    <p><button @click="test($refs)">透過 $refs 給孩子禮物</button></p>
    // 注:空物件
    <p>$refs: {{ $refs }}</p>
    <hr>
    <ChildA ref="c1"/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import {ref} from 'vue'
const c1 = ref()

function sendGift(){
    ...
}

// {[key: string]: any} 表示物件的鍵是字串型別,而值可以是任意型別
function test(v: {[key: string]: any}){
    // v就是傳來的 $refs
    v.c1.gift = '足球'
    console.log('v: ', v);
}
</script>

Tip:模板通點選可以將 $refs 傳入js中,模板中直接透過 $refs 為空(或許是 $refs 是後生成的,並且沒有響應式)。

疑惑:如何在vue3的組合式api的js裡直接取得 $refs?

孩子給父親禮物,可以使用 $parent,最終程式碼如下:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>孩子給的禮物:{{ gift }}</p>
    <p><button @click="sendGift">透過 ref 給孩子禮物</button></p>
    <hr>
    <ChildA ref="c1"/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import {ref} from 'vue'
const c1 = ref()

function sendGift(){
    console.log('c1.value: ', c1.value);
    c1.value.gift = '籃球'
}

let gift = ref('')
// 父親只讓別人訪問gift,其他不允許
defineExpose({gift})

</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>父親給的禮物:{{ gift }}</p>
    <p><button @click="sendGift($parent)">透過 $parent 給父親禮物</button></p>
</template>

<script lang="ts" setup name="App">

import {ref} from 'vue'
const gift = ref('')

function sendGift(parent: any){
    console.log('p: ', parent);
    parent.gift = 'kiss'
}
defineExpose({gift})
</script>

Tip:ref($refs)、$parent 直接操作父元件或子元件的資料,不太好。但某些情況下或許有用。

provide 和 inject

上面我們使用 $arrts 實現了祖孫資料互傳。有個缺點就是會打擾到中間人:父親 —— 在父元件中需要寫 v-bind=$attrs

而 provide/inject 不打擾中間人,實現祖孫資料互傳。請看示例:

祖父透過 provide 提供屬性或方法給後代:

<!-- Gradfather.vue -->
<template>
    <p># 爺爺</p>
    <hr>
    <Father/>
</template>

<script lang="ts" setup name="App">
import Father from './Father.vue';
import {ref,} from 'vue'

function changeAddress(v: string){
    address.value = v
}

import { provide} from 'vue'
let address = ref('長沙')
provide('address', address)
provide('changeAddress', changeAddress)

</script>

父元件透過 inject 能收到祖父提供出來的資料:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <!-- 父元件也能收到資料。provide 能傳給所有後代,不僅僅是孫子 -->
    <p>address {{ address }}</p>
    <hr>
    <ChildA/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

import { inject } from 'vue';

let address = inject('address')
</script>

孫子透過 inject 接收祖父提供的屬性和方法:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>address: {{ address }}</p>
    <p><button @click="changeAddress('北京')">change 祖父 tel</button></p>
</template>

<script lang="ts" setup name="App">

import { inject } from 'vue';

let address = inject('address')
// inject 第二個引數用於設定預設值,用於解決 ts 報錯。
let changeAddress = inject('changeAddress', (v:string) => {})
</script>

瀏覽器呈現:

# 爺爺
——————————————————————
# 父親

address 長沙
——————————————————————
# 元件A

address: 長沙

// 按鈕
change 祖父 tel

長沙來自祖父。點選按鈕,長沙變成北京,實現孫子到祖父的通訊。

升級一下上述示例:祖父提供物件,並將地址和修改地址的方法合併一起傳出。請看示例:

<!-- Gradfather.vue -->
<template>
    <p># 爺爺</p>
    <hr>
    <Father/>
</template>

<script lang="ts" setup name="App">
import Father from './Father.vue';
import {reactive, ref,} from 'vue'

function changeAddress(v: string){
    address.value = v
}

import { provide} from 'vue'
let address = ref('長沙')

let phone = reactive({
    price: 1800,
    color: 'red'
})
// 注:不要 address.value,否則就不是響應式,孩子的address不會變。
provide('addressContext', {address, changeAddress})

// 傳物件
provide('phone', phone)

</script>
<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p>phone.color: {{ phone.color }}</p>
    <hr>
    <ChildA/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
import { inject } from 'vue';
// 隱晦的告訴模板中的 phone.color 是字串型別
let phone = inject('phone', {color: '', price: 0})
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>address: {{ address }}</p>
    <p><button @click="changeAddress('北京')">change 祖父 tel</button></p>
</template>

<script lang="ts" setup name="App">

import { inject } from 'vue';

let {address, changeAddress} = inject('addressContext', {address: '', changeAddress: (v:string) => {}})
// address: RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: '長沙', _value: '長沙'}
// address 已經是響應式的。無需使用 toRefs
console.log('address: ', address);
</script>

插槽

vue2 中就存在這個概念,詳細請看官網:vue3 插槽

具名插槽和預設插槽用於父傳子,作用域插槽用於子傳父

預設插槽

子元件透過 slot 定義插槽。

比較簡單,直接看例子:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    <ChildA>
        click me
    </ChildA>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <slot>預設值</slot>
</template>

如果沒傳,則顯示“預設值”

Tip:父元件使用子元件,比如子元件有標題和內容,標題透過父元件 props 傳遞,內容可以是圖片、影片,列表等等,就可以在父元件中使用插槽。

具名插槽

預設插槽其實就是具名插槽的一種。因為預設插槽也有名字(即default)。

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    <ChildA>
        <!-- 這邊順序隨便(先寫 list2 再寫 list1),最終渲染順序由子元件中的 具名slot 決定(先渲染 list1) -->
        <template #list2>
            <ul>
                <li>c</li>
                <li>d</li>
            </ul>
        </template>

        <!-- 報錯:<ul v-slot:list> -->
        <template v-slot:list1>
            <ul>
                <li>a</li>
                <li>b</li>
            </ul>
        </template>

        <template #default>
            預設插槽的名字叫 default
        </template>
    </ChildA>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'

</script>

透過 name 屬性給插槽定義名字,父元件透過 v-slot 應用對應的插槽。

現在用 v-slot,只能用於元件或 <template> 標籤。用於元件的缺點是:標籤內的全部內容會放在具名插槽上,如果存在多個具名插槽就不行了。

v-slot:list1 簡寫成 #list1

Tip:slot-scope 在2.6廢除了,而在 2.6.x 中,scope、slot和slot-scope 都推薦使用 v-scope。

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <!-- 定義插槽名字-->
    <slot name="list1"></slot>
    <slot name="list2"></slot>

    <slot></slot>
</template>

瀏覽器呈現大概這樣:

# 元件A

- a
- b

- c
- d

預設插槽的名字叫 default

作用域插槽

作用域插槽(scoped slots)的主要作用是允許父元件在插槽內容中訪問子元件中的資料或方法。

資料在子元件,但資料生成的結構,由父元件決定

作用域插槽,UI元件庫用的很多,比如 table、對話方塊。寫過 table的通常會用插槽。表格某列的結構由我們決定。資料我們會傳給ui元件。

Tip:為什麼叫作用域插槽?可以這麼理解:父元件中需要訪問孩子的資料,但是有作用域的限制,於是用這個作用域插槽解決。

作用域插槽有點子傳父的感覺。因為在父元件中用到了子元件的資料

請看這個簡單的示例:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    <ChildA>
        <template v-slot="myProps">
            <div>
                父元件定義結構,資料來自孩子:{{ myProps }}
            </div>
            
        </template>
    </ChildA>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <slot :age="age"></slot>
</template>

<script lang="ts" setup name="App">
import {ref} from 'vue'

let age = ref(18)
</script>

瀏覽器呈現:

# 元件A

父元件定義結構,資料來自孩子
{ "age": 18 }

Tip:有人覺得作用域插槽難,其實是因為寫法多。比如:

可以直接解構:

<ChildA>
    <template v-slot="{age}">
        父元件定義結構,資料來自孩子:{{ age }}
    </template>
</ChildA>

在加上具名插槽:

<ChildA>
    <!-- 簡寫: <template #p1="{age}"> -->
    <template v-slot:p1="{age}">
        父元件定義結構,資料來自孩子:{{ age }}
    </template>
</ChildA>

總結

  • 父傳子:props、v-model、$refs、插槽
  • 子傳父:props、自定義事件、v-model、$parent、作用域插槽
  • 祖孫互傳:$attrs、provide/inject
  • 兄弟和任意元件:mitt、pinia(Pinia 是一個基於 Vue 3 的狀態管理庫,可代替vuex,配合 vue3 使用)

擴充套件

v-bind

v-bind 最基本用途是動態更新html元素上的屬性,比如 id

div v-bind:id="dynamicId"></div>

<!-- 縮寫成 -->
<div :id="dynamicId"></div>

還可以寫物件:

<a-form-model v-bind="{a: 100, b:200}">

等價於

<a-form-model 
  :a="100" 
  :b="200"
>

事件傳參

先複習下vue2 事件傳參

<!-- 什麼都不傳 -->
<button v-on:click="greet()">Greet</button>
<!-- 預設會傳遞一個原生事件物件 event -->
<button v-on:click="greet">Greet</button>
<!-- $event 是Vue 提供的一個特殊變數,表示原生事件物件 -->
<button v-on:click="greet('hello', $event)">Greet</button>

vue3 中也一樣。請看示例:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <p><button @click="test(1,2)">test(1,2)</button></p>
    <!-- $event 就是一個佔位符,會傳入事件物件 -->
    <p><button @click="test(1,2, $event)">test(1,2, $event)</button></p>
    <p><button @click="test2">test2</button></p>

</template>

<script lang="ts" setup name="App">

// c 是undefined
function test(a: number, b: number, c?: Event){
    console.log('a', a, 'b', b)
    console.log('c', c)
}
// 
function test2(a:Event){
    // a PointerEvent {isTrusted: true, _vts: 1713164112622, pointerId: 1, width: 1, height: 1, …}
    console.log('a', a)
}
</script>

Tip$event 是一個特殊的佔位符,比如這樣也會觸發:@click="a = $event"

$event 能否 .target

對於原生事件,$event是事件物件,就能 $event.target.value

對於自定義事件,$event就是觸發事件時傳來的資料,就不能 .target

ref

訪問 ref 資料到底要不要 .value?

如果ref 是你定義的,例如 let name = ref('Peng'),讀取name就得加 .value,如果你要訪問的 ref 是某個響應式資料內的屬性,就不要 .value。就像這樣:

let obj = reactive({
    name: 'Peng',
    o: ref('18')
})
// Peng 18
console.log(obj.name, obj.o);

vscode 報錯如何檢視

用vscode 編碼時,有時會出現紅色波浪線,移上去有很多提示。看你能看懂的。比如中間是很多程式碼,最後一點中文,可能透過中文你就知道報錯原因。

其他章節請看:

vue3 快速入門 系列

相關文章