2023-11-06|閱讀時間 ‧ 約 15 分鐘

Vue3筆記 | 元件與資料傳遞

Vue 的整個大架構核心是元件 (component),藉由把會重複利用的邏輯、樣式、內容封裝到一個個組件之中,來提高程式碼複用性以及開發效率。簡單來說,一個 Vue 建立的網頁就是由許多組件一個個拼出來的,然後資料在各個組件中穿梭來改變渲染我們的畫面。

所以本篇核心為 Vue 的元件以及資料傳遞。



元件

讓我們先在 src/components 下建立一個 PeopleBlock.vue 的檔案,這之後就會是我們第一個元件。

我們先在 PeopleBlock.vue 中寫一點東西:

<script setup>
</script>

<template>
<h1>I am a component</h1>
</template>

然後回到 App.vue 中來引入剛剛建立的元件,此時 App.vue 相對於 PeopleBlock.vue 來說就是父元件。引入成功會看到畫面改變啦!

<script setup>
import PeopleBlock from './components/PeopleBlock.vue';
</script>

<template>
<PeopleBlock />
</template>

這種是元件引入的基本用法,但如果說今天 PeopleBlock.vue 要在很多元件中引入呢?每個元件都寫一次 import好麻煩,所以針對這類情形可以直接修改 main.js。

原本的 main.js 內部長這樣:

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

現在我們要把它拆開改成這樣:

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import PeopleBlock from './components/PeopleBlock.vue'

const app = createApp(App)
app.component('PeopleBlock', PeopleBlock)

app.mount('#app')

這樣我們可以直接在 App.vue 中直接使用 PeopleBlock.vue 而不用 import



資料傳遞

Props

如果你跟我一樣從 React 過來的,那看到這個名詞會覺得很熟悉,這就是父元件將資料傳給子元件的方式,注意,是父元件傳給子元件

現在假設我們父元件 App.vue 中有一筆資料要傳到 PeopleBlock.vue 中套用它的格式,那麼在兩個檔案中我該如何寫?

App.vue:

<template>
<PeopleBlock name="Jeremy"/>
</template>

PeopleBlock.vue:

<script setup>
const props = defineProps(['name'])
</script>

<template>
<h1>My name is: {{ props.name }}</h1>
</template>

你會發現父元件中給的名字 Jeremy 成功傳給子元件做出了渲染。

在 Vue 中靠 defineProps 在子組件中定義接收到父元件資料,可以以陣列或物件的方式呈現。物件的寫法比較嚴謹一些,剛剛的宣告改成物件可以這樣寫:

const props = defineProps({
name:{
type: String,
default: ''
}
})

emit

在講 emit 之前,我要先提一個我稍早跟 senior 討論的問題:到底能不能把 function 在 Vue 中作為 props 傳遞給子元件?

從 React 過來的人應該通常都有這問題吧!因為 React 就是這樣做啊,我為什麼還要用一個 emit 來做傳遞?而且實際操作也可以喔,請看:

App.vue:

<script setup>
import { ref } from 'vue';

const name = ref('Jeremy')

const changeName = (newName)=> name.value = newName
</script>

<template>
<PeopleBlock :name="name" :change-name="changeName"/>
</template>

PeopleBlock.vue:

<script setup>
const props = defineProps({
name:{
type: String,
default: ''
},
changeName:{
type: Function,
default: () => {}
}
})
</script>

<template>
<h1>My name is: {{ props.name }}</h1>
<button @click="changeName('Joanna')">click</button>
</template>

會發現實際上當我點擊了按鈕後,名字從 Jeremy 變成 Joanna 了,一切貌似運作正常。而且 Mike 老師也在影片中特別說這樣單向資料流的方式便於維護管理。

但是喔,就是這個但是,Vue 不推薦你這樣做。部分文章也指出這樣做第一個有效能的問題,第二個是 Vue 看心情可能會渲染不出來改變的資料,所以為了避免這種賭運氣的事件,公司的 senior 還是統一用 emit了。

所以現在來看看 Vue 不同於 React 的做法,有一個口訣:

Props in, Events out

Props 大家很熟悉,events out 靠的就是 emit,是由子組件把事件傳給父元件,然後由父組件去監聽這個事件來決定要幹什麼。

比如剛剛的 code,改成 emit 操作會是這樣的:

先改 PeopleBlock.vue:

<script setup>
const props = defineProps({
name:{
type: String,
default: ''
}
})

defineEmits(['update-name'])
</script>

<template>
<h1>My name is: {{ props.name }}</h1>
<button @click="$emit('update-name')">click</button>
</template>

解釋一下就是我在子元件中建立一個客製化事件叫做 update-name,並透過 $emit 將這個事件傳給父元件。

App.vue 修改如下:

<script setup>
import { ref } from 'vue';

const name = ref('Jeremy')

const changeName = (newName)=> name.value = newName
</script>

<template>
<PeopleBlock :name="name" @update-name="changeName('Joanna')"/>
</template>

在父元件中透過 @update-name 接收從子元件傳來的事件,若事件被觸發就套用父元件本身設定的 changeName 函式更新名字。

這就是 Vue 有別 React 的資料傳遞方式,這種模式還會在之後的 v-model 在元件中的傳遞也息息相關,是一個必須要搞懂的地方。

v-model

過往在取得表單的資料通常都要寫一大串的 DOM 抓取或是監聽 change 或 input 事件,而在 Vue 中,這些事情變得非常簡單,靠的就是 v-model

我們來建立一下一個 <input> 嘗試看看:

<script setup>
import { ref } from 'vue';

const name = ref('Jeremy')
</script>

<template>
<input type="text" v-model="name">
<h1>{{ name }}</h1>
</template>

可以輕鬆發現隨著我在輸入框的改變,<h1> 渲染的畫面也會跟著改變,這表示 v-model 確實地把這個 <input> 跟我的 name 這個 ref 項目綁在一起了,這就是 Vue 在表單處理上強大的地方。

你甚至可以把 v-model 用在 check box、textarea、radio…等地方。

而且 v-model 還提供三種修飾福來幫助處理表單:

  1. .lazy:這個會把 Vue 的同步更新事件從預設的 input 改成 change,也就是你在輸入的時候並不會立即觸發畫面更新。
  2. .number:把輸入資料轉成數字,但如果開頭為0,他會幫你拿掉。
  3. .trim:我感覺最有用的地方,去除首尾空白!再也不用取出input value 後才寫 trim 了!

但是最前面講到的,Vue 是一個元件組成的事件,有時甚至會把這樣一個 <input> 單獨寫成一個元件,那 v-model 勢必得在元件中傳遞,該怎麼做呢?回歸剛剛講到的 Props in, Events out,來看一下如何利用這個口訣解決問題。

先創個 InputArea.vue 元件然後先不寫任何資料:

InputArea.vue:

<script setup >
</script>

<template>
<input type="text">
<h1></h1>
</template>

App.vue:

<script setup>
import InputArea from './components/InputArea.vue'

</script>

<template>
<InputArea/>
</template>

現在我們來建立資料進行傳遞:

App.vue:

<script setup>
import { ref } from 'vue';
import InputArea from './components/InputArea.vue'

const name = ref('Jeremy')
</script>

<template>
<InputArea v-model="name"/>
</template>

父元件的操作很簡單,綁 v-model 就對了。

InputArea.vue:

<script setup >
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
<input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
<h2>{{ modelValue }}</h2>
</template>

我們在子元件上定義接收資料的 props 與傳送事件的 emit,然後在 <input> v-vindvalue 屬性和 input 事件,input 事件中帶上我們自定義的事件,這樣就成功讓 v-model 在兩個元件中傳遞。

那能不能綁定多個 v-model呢?讓我抄一下官網的 code :

App.vue:

<script setup>
import { ref } from 'vue';
import InputArea from './components/InputArea.vue'

const first = ref('Jeremy')
const last = ref('Ho')
</script>

<template>
<InputArea
v-model:first-name="first"
v-model:last-name="last"/>
</template>

InputArea.vue:

<script setup>
defineProps({
firstName: String,
lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
<h1>{{ firstName }} {{ lastName }}</h1>
</template>

你可能注意到,上面兩個 v-model 的範例中,App.vue 中對 v-model 的綁定寫法不太一樣。一個是直接 v-model="變數",一個是 v-model:名字="變數",對於前面那種作法,你在子元件中 defineProps 時就只能用 modelValue,而後面那一種允許你寫自己定義的名字,這是差別所在。



參考資料

  1. Vue.js 官方網站
  2. Vue3 + Vite 快速上手 Get Startrd EP3 - components / props / emit
  3. Vue3 + Vite 快速上手 Get Startrd EP4 - v-model 資料的雙向綁定 / 自訂義組件的資料綁定 / One-Way Data Flow 單向資料流
  4. Vue 中,如何将函数作为 props 传递给组件
  5. 父子組件資料傳遞 props、$emit
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.