關於 Vue Props 單向資料流這件事

Vue

前言

這一篇文章算是稍微記錄一下 Vue 關於 Props 單向資料流的有趣狀況以及該如何解決。

Props

首先我們先來看一下範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref } from 'vue'
import A from './components/A.vue'

const data = ref('Hello');
</script>

<template>
<div class="container">
<p>
App.vue:{{ data }}
</p>
<div class="mb-3">
<label for="text" class="form-label">App 的資料:</label>
<input v-model="data" type="text" class="form-control" id="text">
</div>
<hr>
<A :msg="data" />
</div>
</template>

<style scoped>
</style>

我們可以看到這邊我們引入了一個元件叫做 A.vue,然後傳入了 msg 這個 Props。

而目前底下 A.vue 的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
const props = defineProps({
msg: {
type: String,
}
})
</script>

<template>
<div class="p-4 border border-success">
<p>A.vue:{{ msg }}</p>
<div class="mb-3">
<label for="text" class="form-label">App 的資料:</label>
</div>
</div>
</template>

<style lang="scss" scoped>
</style>

在上面畫面上,我們可以看到當我們在 Input 上修改資料時,A.vue 的資料也會跟著改變。

那麼接下來我們來嘗試做一件事情,那就是關於 Vue 的單向資料流的部分,所以我們來修改一下 A.vue 的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
const props = defineProps({
msg: {
type: String,
}
})
</script>

<template>
<div class="p-4 border border-success">
<p>A.vue:{{ msg }}</p>
<div class="mb-3">
<label for="text" class="form-label">App 的資料:</label>
<input v-model="props.msg" type="text" class="form-control" id="text">
</div>
</div>
</template>

<style lang="scss" scoped>
</style>

其實就只是單純補上 input 的部分,然後把 v-model 的值改成 props.msg

當你嘗試修改位於 A.vue 的 Input 欄位時,你會發現 App.vue、A.vue 的資料都沒有改變,而且 Console 上也會出現錯誤訊息。

[Vue warn] Set operation on key “msg” failed: target is readonly.
{msg: “Hello”}

Console

而這就是我們所熟悉的單向資料流,資料只能由父層傳遞到子層,而子層不能直接修改父層的資料,如果要修改的話就必須透過 Emit 事件的方式來進行,避免資料流的混亂。

有趣的狀況

接下來我們來看一個有趣的狀況,我們來修改一下 App.vue 的程式碼,與建立一個 B.vue 的元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script setup>
import { ref } from 'vue'
import A from './components/A.vue'
import B from './components/B.vue'

const data = ref('Hello');

const data2 = ref({
msg: 'Hello2',
});
</script>

<template>
<div class="container">
<p>
App.vue:{{ data }}
</p>
<div class="mb-3">
<label for="text" class="form-label">App 的資料:</label>
<input v-model="data" type="text" class="form-control" id="text">
</div>
<hr>
<A :msg="data" />
<hr>
<B :msg="data2" />
</div>
</template>

<style scoped>
</style>

而新建立的 B.vue 的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
const props = defineProps({
data: {
type: Object,
},
});
</script>

<template>
<div class="p-4 border border-success">
<p>B.vue:{{ props.data.msg }}</p>
<div class="mb-3">
<label for="text" class="form-label">B 的資料:</label>
<input
v-model="props.data.msg"
type="text"
class="form-control"
id="text"
/>
</div>
</div>
</template>

<style lang="scss" scoped>
</style>

接著有趣的事情發生了,前面我有示範過當我修改 A.vue 的 Input 時,App.vue、A.vue 的資料都沒有改變,而且會被阻擋起來,也就是剛剛說的單向資料流,但是當我修改 B.vue 的 Input 時…狀況就不一樣了…

Input

說好的單向資料流呢?為什麼會這樣呢?其實原因很簡單,因為對於 Vue 的 Props 來講,它所監聽的記憶體位置,而我們並沒有修改記憶體位置,所以 Vue 並不會知道我們有修改資料,所以就不會阻擋我們的修改,那我這邊所謂的記憶體位置是指什麼呢?主要是指 data2 這個變數,而這個變數是一個物件,所以 Vue 會監聽這個物件的記憶體位置,而我們並沒有修改這個物件的記憶體位置,所以 Vue 並不會知道我們有修改資料,所以就不會阻擋我們的修改。

所以你要說這是 Vue 的 Bug 嗎?我認為不是,我認為這是 JavaScript 本身的一個特性,當你對一個物件底下的屬性進行修改時,你並沒有修改這個物件的記憶體位置,所以 Vue 並不會知道你有修改資料,所以就不會阻擋你的修改。

因此基於這個觀念來講,如果 B.vue 修改成以下就會發生錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup>
/* global defineProps */

const props = defineProps({
data: {
type: Object,
},
});
</script>

<template>
<div class="p-4 border border-success">
<p>B.vue:{{ props.data.msg }}</p>
<div class="mb-3">
<label for="text" class="form-label">B 的資料:</label>
<input
v-model="props.data"
type="text"
class="form-control"
id="text"
/>
</div>
</div>
</template>

<style lang="scss" scoped>
</style>

因為在此我們修改的是 props.data 這個物件,所以就會發生錯誤。

props.data

解決方式

看完了上面的範例,那麼如果我們就是想要傳一個物件下來呢?那該怎麼解決呢?其實解決方式很簡單,使用以下任何一種語法都可以:

  • JSON.parse(JSON.stringify(data))
  • Object.assign({}, data)
  • {...data}

這邊我就示範深層拷貝的方式,也就是 JSON.parse(JSON.stringify(data))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- C.vue -->
<script setup>
/* global defineProps */
import { ref } from "vue";

const props = defineProps({
data: {
type: Object,
},
});

const data = ref(JSON.parse(JSON.stringify(props.data)));
</script>

<template>
<div class="p-4 border border-success">
<p>C.vue:{{ props.data.msg }}</p>
<p>data:{{ data }}</p>
<div class="mb-3">
<label for="text" class="form-label">C 的資料:</label>
<input v-model="data.msg" type="text" class="form-control" id="text" />
</div>
</div>
</template>

<style lang="scss" scoped>
</style>

透過這種方式,你就可以解決物件傳參考的問題了。

如果你經驗比較豐富一點的話,你可能會使用 toRefs 來取得 Props 的值,但是 toRefs 只支援 Proxy 的物件,因此是不能使用深層拷貝的方式來解決的,但如果你使用淺層拷貝,那就一樣會發生修改到 Props 的值的問題哩。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ