iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 10

不只懂 Vue 語法:什麼是單向資料流和雙向綁定?

問題回答

雙向綁定(two-way data bindings)是指把畫面上的 DOM 與資料透過 Vue 實體來綁定。當其中一方有更動時,另一方都會隨即更新。

單向資料流是 Vue 官方提倡的一種管理元件資料狀態模式。重點在於,當子層元件透過 props 來接收父層元件傳來的資料時,子層不應該直接修改 props,不然開發時就難以追蹤資料狀態的變化。建議做法是透過 emit,觸發事件來修改在父層的資料。也就是常見口訣:「props in, event out」的意思。

以下會再詳細解說雙向綁定、單向資料流、以及使用 props 時須注意的事。

雙向綁定

雙向綁定的重點就是當畫面或資料有更新,對方也會隨之更新。最明顯的例子就是 v-model,我們很常在 input 欄位上 v-model 來綁定畫面中輸入欄目前的內容,以及在 data 裏的資料。一旦用戶輸入內容,我們的 Vue 裏的 data 資料也會同步擁有相同的資料,反之亦然。

單向資料流

單向資料流的概念是針對父子層元件的資料傳遞時的模式,它跟雙向綁定的概念並沒有衝突。當要把資料從父元件傳遞到子元件時,子元件會使用 props 來接收,但子元件不應該直接修改 props 來修改父元件的資料 ,而是使用 emit。

假如你是傳入基本型別的資料,例如數字、字串等,你在子元件不會修改到父元件的資料,因為當父元件再被渲染時,它依舊要指向父元件所定義的資料,像以下例子:

HelloWorld.vue(子層)

<input v-model="childMsg" />
export default {
  props: {
    value: String,
    childMsg: {
      type: String,
      default: "default msg"
    }
  }
};

App.vue (父層)

<template>
  <div>
    {{ parentMsg }}
    <HelloWorld :childMsg="parentMsg" />
  </div>
</template>
export default {
  name: "Home",
  components: {
    HelloWorld
  },
  data() {
    return {
      parentMsg: "Hello vue!"
    };
  }
};

警告:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

父層的 parentMsg 並沒有被修改,依舊為 "Hello vue!"。但 Vue 會把你以上的行為視作嘗試修改父層資料,因此跳警告提示此舉是無效。

如果是資料是物件呢?會否跳警告?

在父元件加入物件資料:

App.vue

data() {
    return {
      parentMsg: "Hello vue!",
      parent: {
        a: 123
      }
    };
}

綁定 props:

<template>
  <div>
    <p>{{ parentMsg }}</p>
    <p>{{ parent }}</p>
    <HelloWorld :childMsg="parentMsg" :child="parent" />
  </div>
</template>

HelloWorld.vue(子元件):

<template>
  <div>
    <input v-model="childMsg" />
    <input v-model="child.a" />
  </div>
</template>

<script>
export default {
  props: {
    value: String,
    childMsg: {
      type: String,
      default: "default msg"
    },
    child: {
      type: Object,
      default: {}
    }
  }
};
</script>

結果會直接改掉父層資料,而且 console 沒有跳警告

官方文件有解釋,這是因為物件和陣列是傳址(pass by reference),因此會直接修改到父元件的資料。官方不建議這做法。

在 Vue 3,如果你的 Vue CLI 專案有加入 ESLint,當你的子元件的 input ,使用 v-model 直接綁定 props時,ESLint 就都會報錯:

Unexpected mutation of "child" prop

但如果沒有加入 ESLint,不管是修改物件還是基本型別資料,也不會報錯,而且也能把父元子的資料改掉。

在 props 傳送物件型別資料時,須注意的事項

為了保持單向資料流,就不能在子元件綁定 props。

如果 props 是基本型別資料,那就在 data 裏建立屬性來拷貝 props 的值即可,或者使用 computed 來處理資料再掉回畫面使用,這些都是官方文件有提到的做法,因此就不作解釋了。

但如果資料是物件或陣列,考慮到傳址的問題,可以用以下方式解決:

1. 使用 v-bind,自動解構賦值

如果物件資料的屬性不多的話,可以用 v-bind 來處理。示範如下:

假設在父元件有一筆物件資料,並傳送到子元件的 input 裏:

App.vue 資料(父元件):

  data() {
    return {
      user: {
        name: "Tom",
        age: 20,
      },
    };
  },

傳入子元件(Child1):

<Child1 v-bind="user" />

在子元件裏,設定 nameage 這兩個 props,解構 user 物件並賦值到這兩個 props 裏,最後再在 data 拷貝這兩個 props:

<template>
  <div>
    <input type="text" v-model="userName">
  </div>

  <div>
    <input type="text" v-model="userAge">
  </div>
</template>

<script>
export default {
  props: ["name", "age"],
  data() {
    return {
      userName: this.name,
      userAge: this.age,
    }
  }
};
</script>

但如果該物件資料的屬性很多,就需要寫很多個 props,因此此方法就不適合。

2. 使用 JSON.parse 處理

遇上物件屬性很多的情況,我們可以直接把整個物件傳進去子元件,並在 data 裏使用 JSON.parse()JSON.stringify() 深拷貝這個物件,最後在 input 綁定拷貝的結果:

Child2(子元件):

<template>
  <div>
    <input type="text" v-model="user.name" />
  </div>

  <div>
    <input type="text" v-model="user.age" />
  </div>
</template>

<script>
export default {
  props: ["userInfo"],
  data() {
    return {
      user: JSON.parse(JSON.stringify(this.userInfo)),
      // 用展開語法也可以,但注意不要用在處理多層物件,但因為只能做淺拷貝
      // user: {...this.userInfo}
    };
  },
};
</script>

完整程式碼示範兩種方法

https://codesandbox.io/s/vue-chuan-wu-jian-props-shi-de-v-bind-jie-gou-he-json-parse-fang-fa-ghxfr?file=/src/components/Child.vue

props 的補充知識

使用 v-bind 才能傳入數字

當我們要以 props 傳入數字,必須要使用 v-bind 才可以。沒有使用 v-bind 的話,一律當作傳入純字串。

字串:

<HelloWorld num="3" />

數字

<HelloWorld :num="3" />

注意,:v-bind 的縮寫。

props 驗證設定

官方在 style guide 提及,不建議以下的寫法:

props: ['status']

Vue 建議用物件方式,加入型別檢查、預設值等等:

props: {
  status: {
    type: String,
    required: true,
    validator: function (value) {
      return [
        'syncing',
        'synced',
        'version-conflict',
        'error'
      ].indexOf(value) !== -1
    }
  }
}

這是為了更嚴謹傳入 props,減少錯誤。因為當錯誤傳入 props時,Vue 會跳警告提醒。在例子中,如果 validator 回傳 false,同樣會跳黃字警告。

另外,有些情況我亦會使用 default 屬性來設定預設值。例如在呼叫 API 時,需要等一筆陣列資料回傳過來,再放入 props 裏傳給子元件,再在子元件使 v-for 顯示陣列裏每一筆的資料。如果在子元件的 props 先建立預設值,那麼使用 v-for 來綁定 props 時就不會報錯。

總結

  • 雙向綁定是指把畫面上的 DOM 與資料透過 Vue 實體來綁定。只要對方有更新,另一方也會更新。
  • 單向資料流是管理父子元件資料狀態的模式,資料只能由父元件傳列子元件,子元件透過 props 來接收,但子元件不能以直接修改 props 的方式,改變父元件傳來的資料。
  • 在子元件要改變父元件的資料,就要使用 emit 來實現。
  • 如果 props 的資料是物件型別,並需要把它放到 data 裏使用的話,可以使用 v-bind 自動解構賦值,或者是深拷貝方法 JSON.parse()JSON.stringify 來處理。
  • 設定 props 時,建議用物件方式來建立,而非陣列。從而更嚴謹和仔細處理 props 的資料。

參考資料

重新認識 Vue.js - 2-2 元件之間的溝通傳遞


上一篇
不只懂 Vue 語法:為何元件裏的 data 必須是函式?建立 data 時能否使用箭頭函式?
下一篇
不只懂 Vue 語法:如何使用 v-model 實現父子元件傳遞資料?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31

尚未有邦友留言

立即登入留言