# 組件之間的溝通傳遞

提醒

注意 prop 在初始化時的順序會優於 datacomputed 等屬性,所以像 props 中的 defaultvalidator 是無法取得實體內的這些狀態。

# props

主要的核心概念:

  • 多數用於 一次性 的設定值,沒有一定要雙向綁定的需求

  • 這些組件都已經是末端組件,換句話說,子組件下不太會有其他子組件存在

  • 會搭配 Slot 來做到組件變化,而不是再寫一個子組件

  • 當你必須要使用其他子組件時,請改用事件傳遞所需要的資料

  • props 所承接的資料,絕大多數不會過於複雜

    以分頁為範例,應該將各分頁的資料分開來寫,不要全部傳入子組件。

    子組件

    // bad - 傳入整個物件
    export default {
      name: 'Pagination',
      props: {
        pager: {
          type: Object,
          required: true
        }
      }
    };
    // good - 屬性分開傳入
    export default {
      name: 'Pagination',
      props: {
        totlaItems: {
          type: Number,
          required: true
        },
        limit: {
          type: Number,
          required: true
        },
        first: {
          type: Number,
          required: true
        },
        current: {
          type: Number,
          required: true
        }
      }
    };
    
    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
    31
    32

    父組件

    <!-- bad -->
    <pagination :pager="pager"></pagination>
    
    <!-- good -->
    <ul>
      <li v-for="page in pager">
        <pagination v-bind="page"></pagination>
      </li>
    </ul>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 傳入 props 時沒有加上 v-bind

一律以「純文字」的形式在子組件被接收,而不是外層組件的狀態。

實務上,除了忘記加上 v-bind 指令的情況外,通常會使用在希望後端直接輸出網頁內容時, 預先將傳入子組件的內容印在 HTML 的標籤上,這樣可以節省掉一次 request。

# 以物件作為 props 傳遞

由於 JavaScript 的物件是以「參考」的方式來傳遞的 (pass by reference) ,所以若是要由外層組件傳遞物件至內層子組件時,則需要特別小心。

<edit-component
  v-for="comic in comics"
  :key="comic.id"
  :comic="comic"
></edit-component>
1
2
3
4
5
試一試
  • 鬼滅之刃
  • 2020/07/09
  • 我的英雄學院
  • 2020/07/10

乍看之下感覺沒什麼問題,但是此時若我們嘗試在子組件對 input 進行修改, 就會發現外層的資料也被變動了!

這時就可能由於某個子組件的修改,卻造成另一個子組件的 props 狀態污染,產生難以追蹤且不可預期的錯誤了。

所以,想要傳遞物件類型的 props 屬性時,先將物件屬性解構後再傳遞出去:




 
 
 


<edit-component
  v-for="comic in comics"
  :key="comic.id"
  :id="comic.id"
  :name="comic.name"
  :time="comic.time"
></edit-component>
1
2
3
4
5
6
7

如果覺得把所有的屬性打散,可以透過 v-bind 指令,達到自動將物件解構效果:




 


<edit-component
  v-for="comic in comics"
  :key="comic.id"
  v-bind="comic"
></edit-component>
1
2
3
4
5
試一試
  • 鬼滅之刃
  • 2020/07/09
  • 我的英雄學院
  • 2020/07/10

# 父組件存取子組件的 DOM 元素

如需要在子組件取得父層組件的內容,可以透過 this.$parent 來存取他的父層組件,反過來則是可以在外層透過 this.$children 來取得子組件的內容。

另外,由於 this.$children 是以「陣列」的形式產生, 而這個陣列有可能因為 v-if 或其他操作造成 this.$children 索引值的更動, 此時我們可以在子組件上加上 ref 屬性作為別名:

<div class="parent">
  <child-component ref="child"></child-component>
</div>
1
2
3

這樣就可以在父層透過 this.$refs.child 就可以來存取指定的子組件了。

# 不是 Props 的 DOM 屬性

如果在組件上設定屬性並沒有在組件中的 props 被定義,這個屬性會直接套用在組件的 根元素 上。

範例如下方。

# 合併父組件及子組件屬性

classstyle 在合併父組件及子組件時會將父子組件所有的屬性合併。

子組件

加上 class="thick"

Vue.component('kebab-case-converter', {
  ...
  template: '<div class="thick">kabeb-case : {{kababCase}}</div>'
})
1
2
3
4

父組件

加上 class="italic"

<kebab-case-converter
  class="italic"
  data-test="test data"
  :camel-case="camelCase"
></kebab-case-converter>
1
2
3
4
5

結果

父組件的 class 屬性會跟子組件的 class 做合併。

<div class="thick italic" data-test="test data">kabeb-case : hello</div>
1

# 由父組件替代子組件的屬性

除了 classstyle 之外的其他屬性都會是由父組件蓋掉子組件屬性值

子組件

<div class="thick" data-test="child">kabeb-case : {{kababCase}}</div>
1

父組件

<kebab-case-converter
  class="italic"
  data-test="parent"
  :camel-case="camelCase"
></kebab-case-converter>
1
2
3
4
5

結果

<div data-test="parent" class="thick italic">kabeb-case :</div>
1

# 避免替代屬性

父組件的屬性蓋掉子組件的值有時會產生非預期的結果,為了避免這樣的問題, Vue 提供了 inheritAttrs 這個參數,它可以將組件設為不要帶入父組件的屬性值(只有那些沒有設定於子組件 porps 中的屬性值不會被帶入)。

inheritAttrs 常常搭配 $attrs 這個物件設定, $attrs父組件的屬性集合(沒有包含 props 中的屬性值),有時我們不想將屬性值設於根元素中,可以使用 inheritAttrs 取消綁定屬性到根元素的行為,並且使用 $attrs 將屬性值綁定要我們期望的元素上。

注意

classstyle 不會受 inheritAttrs 效果影響,也不會包在 $attrs 物件上。

子組件


 



















Vue.component('base-checkbox', {
  inheritAttrs: false,
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
          <div>
            <input
              type="checkbox"
              v-bind="$attrs"
              v-bind:checked="checked"
              v-on:change="$emit('change', $event.target.checked)"
            >
          </div>
        `
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

父組件

<base-checkbox
  v-model="lovingVue"
  class="thick"
  data-test="parent"
></base-checkbox>
1
2
3
4
5

結果

<div class="thick">
  <input type="checkbox" data-test="parent" />
</div>
1
2
3
  • classinheritAttrs$attrsclass 無效,所以還是在 根元素 上。
  • data-testinheritAttrs 的影響,所以 不會 出現在根元素上,但因 $attrs 綁定,所以會在 input 上。

# 客製組件的 v-model

v-modelv-bind:valuev-on:input 的語法糖,它的作用是做 雙向綁定,但有些像是 radiocheckbox 需要 value 去綁定各別選項的值,而真正勾選的值是由像是 checked 之類的屬性綁定,為了避免 v-model 產生衝突,可以將 model 改為 v-bind:checkedv-on:change 來讓 v-model 運作正常。

checkbox 的 v-model














 
 






Vue.component('base-checkbox', {
  model: {
    // 預設為 value
    prop: 'checked',
    // 預設為 input
    event: 'change'
  },
  // 跟 value 一樣, v-model 的 prop : checked 要設定在 props 中
  props: ['checked', 'label'],
  template: `
    <label>
      <input
        type="checkbox"
        :checked="checked"
        @change="$emit('change', $event.target.checked)"
      >
      {{label}}
    </label>
  `
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# model 物件

這個物件有兩個屬性 propevent :

  • prop : 目標屬性。
  • event : 監聽的事件。

# 綁定原生事件

替整個組件加上事件(clickfocus),需使用 .native 修飾詞。

<base-input @focus.native="onFocus"></base-input>
1

但如果當 base-input 為:

 






 

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  />
</label>
1
2
3
4
5
6
7
8

因為根元素是 <label> 而非 <input> ,所以 @focus.native 會綁定到 <label> 導致 focus 事件不會被觸發,為了解決這個問題, Vue 提供了 $listeners ,這個物件包含 所有 的父組件事件(排除有 native 修飾符的事件),如此一來我們就可以使用 $listeners 綁定想要的元素。

修改子組件



 


<label>
  {{label}}
  <input v-bind="$attrs" v-bind:value="value" v-on="$listeners" />
</label>
1
2
3
4

將父組件中的 focus 事件 拿掉 native 修飾符。

<base-input-with-label @focus="onFocus"></base-input-with-label>
1

# 切分 $listeners 的多個事件

子組件

Vue.component('base-input-with-label', {
  props: ['value'],
  computed: {
    // 透過 DevTool 可以看到,只有 input 事件是可以觸發的
    // 其它的因為未定義,而是 invoke
    inputListeners() {
      const vm = this;
      // 使用 Object.assign 合併物件
      return Object.assign(
        {},
        // 將 $listeners 的事件當作預設值
        this.$listeners,
        {
          // 覆蓋 $listeners 中的 input 事件
          input(event) {
            // 這時候的 this 為原生 window 原生事件
            vm.$emit('input', event.target.value);
          }
        }
      );
    },
    focusListeners() {
      const vm = this;
      return Object.assign({}, this.$listeners, {
        // 覆蓋 $listeners 中的 focus 事件
        focus(event) {
          console.log('this is from child focus event');
        }
      });
    }
  },
  template: `
          <label>
            {{value}}
            <!--只會觸發 focus 事件-->
            <input v-on="focusListeners">
          </label>
        `
});
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
31
32
33
34
35
36
37
38
39

父組件

<base-input-with-label @focus="onFocus" v-model="label"></base-input-with-label>
1

# 參考

屬性注意事項 (opens new window)

客製事件 (opens new window)

組件之間的溝通傳遞 (opens new window)

Last Updated: 2/25/2021, 7:56:51 AM