
前言
這一篇文章算是稍微記錄一下 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”}

而這就是我們所熟悉的單向資料流,資料只能由父層傳遞到子層,而子層不能直接修改父層的資料,如果要修改的話就必須透過 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 時…狀況就不一樣了…

說好的單向資料流呢?為什麼會這樣呢?其實原因很簡單,因為對於 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>
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 這個物件,所以就會發生錯誤。

解決方式
看完了上面的範例,那麼如果我們就是想要傳一個物件下來呢?那該怎麼解決呢?其實解決方式很簡單,使用以下任何一種語法都可以:
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
| <script setup>
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 的值的問題哩。
整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ
Advertisement