iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 15
0
Modern Web

VueJS 從前端到後端系列 第 15

Component 的溝通方式 :props Day 14

部落格同步刊登 [IT 鐵人賽] Component 的溝通方式 :props Day 14

這裡拿 :props 來回鍋複習一下,也順帶的,我們會提及一些關於事件溝通的方式。或者說反過來,講事件溝通,然後順帶提一下 :props 也可以。元件溝通其實一直都是各種框架裡面比較麻煩的事情,昨天提到的 Vuex 其實算是偷吃步,元件之間在沒有狀態管理機制的情況下,多數還是仰賴事件傳播為主。

只是,當元件趨於複雜,事件傳播就相對惱人。


props

之前在提及元件的時候有提到這個屬性,如果忘記的人可以再回去看一下:

Component 基本入門 Day 2

當我們需要將資料傳遞給元件的時候,我們可以利用 props 來達成這件事情,當然請記得,他是屬於單向綁定的,如果需要雙向資料溝通,請加上 .sync 修飾子來達成。這個屬性在 Vue Router 也有一個類似的設定,不過跟元件溝通比較沒有關係,算是路由與元件之間的事情。

再者,使用 props 來溝通,對於比較複雜的元件結構來說,就會變得比較難以維護:

import BComponent from '@/components/BComponent.vue'

export default {
  name: 'AComponent',
  components: {
    BComponent
  }
}
import CComponent from '@/components/CComponent.vue'

export default {
  name: 'BComponent',
  props: {
    id: {
      type: Number,
      required: true
    }
  },
  components: {
    CComponent
  }
}
export default {
  name: 'CComponent',
  props: {
    id: {
      type: Number,
      required: true
    }
  }
}

我相信應該不會有人這麼做(吧)?對於 props 的定義上,他主要的目的是為了傳播一些,對於該元件相對有利用價值,爾或是必須要傳入的數值。就如同你撰寫了一個函式,這個函式可以接受某些參數,或者是必須要傳入某些參數,大概是一樣的道理。所以,利用 props 來向眾多子元件傳遞訊息,就變得過於麻煩且相對不好維護。

但這個方式,在多數第三方套件、插件或是客製化元件中,應用上還是相當頻繁。其實最主要的核心概念是:

  • 多數用於 一次性 的設定值,沒有一定要雙向綁定的需求。
  • 這些元件都已經是末端元件,換句話說,不太會有其子元件存在。
  • 會搭配 Slot 來做到元件變化,而不是再寫一個子元件。
  • 當你必須要使用其他子元件時,請改用事件傳遞所需要的資料。
  • props 所承接的資料,絕大多數不會過於複雜。

上述最後一點其實見仁見智,因為我自己寫過分頁元件,然後我把分頁資料整包餵給了他(這是不好的設計,小朋友不要學)。

<section>
  <ul>
    <li v-for="(page, index) in pager" :key="index">
      <a :href="'?page=' + pager.page">{{ pager.page }}</a>
    </li>
  </ul>
</section>
export default {
  name: 'Pagination',
  props: {
    pager: {
      type: Object,
      required: true
    }
  }
}

雖然看起來好像很簡單,但是其實這樣的 props 設計相當不良。私心建議將你的物件分開來寫,雖然在 <template> 裡面可能會變得較為複雜,但對於後續維護上會比較友善。

export default {
  name: 'Pagination',
  props: {
    totlaItems: {
      type: Number,
      required: true,
      default: 0
    },
    limit: {
      type: Number,
      required: true,
      default: 10
    },
    first: {
      type: Number,
      required: true,
      default: 1
    },
    current: {
      type: Number,
      required: true,
      default: 1
    }
  },
  computed: {
    totalPages () {
      if (this.totlaItems <= 0) {
        return 0
      }
      return Math.ceil(this.totalItems / this.limit)
    },
    last () {
      return this.totalPages
    }
  }
}
<section>
  <ul>
    <li v-for="page in totalPages" :key="page">
      <a :href="'?page=' + page">{{ page }}</a>
    </li>
  </ul>
</section>

上述只是簡單的例子,如果當你的頁碼超過 100 頁的時候,你可以再優化上述的呈現方式。不然,你會拿到 100 個 <li> 好像也是哪裡怪怪的。


事件溝通

在 Vue 的元件當中,對於事件的處理上有四種方式:

  1. $on 用於綁定一個或多個事件(多個事件接受陣列方式傳入)。
  2. $emit 用於觸發一個事件。
  3. $once 用於觸發一個事件,但僅觸發一次。
  4. $off 用於解除綁定一個或多個事件(多個事件接受陣列方式傳入)。

上個段落我們提到了,關於子元件之間的溝通,我們可以透過事件傳播的方式來做。不過,基於生命週期的關係,所以你必須 特別留意 事件在 綁定觸發 以及 解除 的時機點。

對於 $off 解除事件的操作,他與 JavaScript 原生的 removeEventListener 很類似,操作的區別在於:

// 不傳入任何參數,會解除全部的綁定事件。
this.$off()

// 傳入事件名稱,會解除所有跟這個事件名稱相關的綁定事件。
this.$off('hitEvent')

// 傳入事件名稱與回呼函式,會解除該事件名稱與此回呼函數的特定綁定事件。
this.$off('hitEvent', this.hitEventCallback)

通常,我們在決定一個事件是否需要被綁定,有一個大方向的分野:

  • 我一開始就要監聽某個事件。
  • 我需要達成某些 條件 才需要監聽某個事件。

如果依照大方向來說,我們舉個例子:

export default {
  name: 'EventComponent',
  created () {
    // 我一開始就要監聽一個事件,所以我在元件建立時就開始監聽。
    this.$on('EventComponentCreated', this.eventComponentCreated)
  },
  mounted () {
    // 我在元件被加入瀏覽器的 DOM 結構樹的時候,才要監聽事件。
    this.$on('EventComponentMounted', this.eventComponentMounted)
  },
  methods: {
    eventComponentCreated () {
      // 在這裡做一些事件的操作。
    },
    eventComponentMounted () {
      // 在這裡做一些事件的操作。
    }
  },
  beforeDestroy () {
    // 元件被銷毀之前,把監聽事件解除。
    this.$off('EventComponentCreated', this.eventComponentCreated)
    this.$off('EventComponentMounted', this.eventComponentMounted)
  }
}

請留意 事件解除 的地方,如果你的元件是可以被重複使用的,那麼你必須要在元件被銷毀之前,先將事件解除,否則,當這個元件再次被使用時,你的事件綁定會 重複 ,這樣會造成同一個事件呼叫,會做出 2 次以上的事件操作。

另外,還記得我們提過的 <keep-alive> 嗎?

薛丁格的生命週期 Day 12

當你的元件被包含在 <keep-alive> 當中,他就沒有所謂的 beforeDestroy() 這件事情,而且,他的 mounted 會不斷的被呼叫,所以,當你有使用 <keep-alive> 的時候,請注意你的事件綁定與解除。


https://ithelp.ithome.com.tw/upload/images/20190916/20001433H0JoN8YBuj.png

上面的操作示意圖,你覺得我的 $emit 是不是有寫錯?是的,上述四種關於事件操作的方法,在元件當中是僅屬於該 元件內部 可以使用的。所以,當你需要呼叫你的子元件,或甚至呼叫你的父元件,那麼你有以下幾種操作方式:

  • 使用 $refs 來指定子元件呼叫其事件。
  • 使用 $children 來指定子元件呼叫其事件。
  • 使用 $parent 來呼叫父元件所綁定的事件。
  • 使用 $root 來呼叫根元件所綁定的事件。

所以,當你需要跨越不同元件來呼叫事件時,你就得搞清楚先後關係,這樣其實操作起來不甚方便,以上述的例子來說:

export default {
  name: 'App',
  components: {
    HelloKitty
  },
  mounted () {
    console.log('App mounted, emit event after 3 seconds.')
    setTimeout(() => {
      this.$refs.HelloKitty.$emit('changeAge', 30)
      this.$children[0].$emit('changeAge', 30)
    }, 3000)
  }
}

倘若你沒有設定 $refs 或是你不知道你到底有幾個女兒(醒醒把你沒有女兒),那麼你可能就沒辦法呼叫你所想要觸發的事件。爾或者是,你可能會叫到其他人的女兒,或是其他人的兒子突然出來認你做親爹的這種狀況。

export default {
  name: 'SomebodyChild',
  mounted () {
    this.$parent.$parent.$parent.$parent.$emit('call', 'Dad')
  }
}

這些是多元件下所衍生的問題。所以元件自身的事件,除了自己呼叫以外,上下屬關係最好可以控制在一個階級以內,如果要高於兩個階級以上,個人是不建議這麼做的。一方面維護上相當麻煩,二來你絕對有機會呼叫到不對的事件。


EventBus

為了解決多元件的事件傳遞,除了偷吃步使用 Vuex 來交換資料外,我們還可以透過 EventBus 的方式來溝通。何謂 EventBus?簡單來說就是 有個老司機要開車了,大家快上車(欸不對)。 我們有一個集合事件的地方,這個地方有各式各樣的事件等著被呼叫,我們利用單純的 JavaScript 可以實作出一台車,只要有心,人人都是 老司機:

class OldDriver {
  constructor () {
    this.driver = document.createElement('bus')
  }

  $on (event, callback) {
    this.driver.addEventListener(event, callback, false)
  }

  $off (event, callback) {
    this.driver.removeEventListener(event, callback, false)
  }

  $emit (event, payload = {}) {
    this.driver.dispatchEvent(new CustomEvent(event, { detail: payload }))
  }
}

export default new OldDriver()

然後你只要在你的 App.vue 裡面這樣操作:

import EventBus from '@/extends/oldDriver.js'

export default {
  name: 'App',
  components: {
    HelloKitty
  },
  mounted () {
    setTimeout(() => {
      EventBus.$emit('changeAge', 30)
    }, 3000)
  }
}

https://ithelp.ithome.com.tw/upload/images/20190916/2000143375szyU9efZ.png

然後你就會拿到 番號 一個叫做 CustomEvent 的物件,沒錯,因為在原生 JavaScript 當中,使用 dispatchEvent 呼叫時,你必須要傳入 CustomEvent 這個物件,來觸發自定義的事件。所以,你在子元件監聽的當下,會拿到整個 Event 的資料,真正的資料存放位置,是在這個物件的 detail 裡面。

https://ithelp.ithome.com.tw/upload/images/20190916/20001433wPys2uSKwJ.png

當然,市面上也是有很多關於 EventBus 的套件。然而,其實 Vue 自己也可以當作老司機,我們只要 new 一個起來用就可以了。

import Vue from 'vue'
export default new Vue()

https://ithelp.ithome.com.tw/upload/images/20190916/20001433B68YJAYo2F.png

你會發現他連 CustomEvent 都幫你處理好了,你就不用自己處理 detail 的部分,這個老司機是不是好棒棒。


我們這樣看下來,是不是覺得 EventBus 好像非常厲害,但是 因為大家都在同一台車上 ,還是有一些事情需要大家留意,我們以使用 new Vue() 為例:

  • 事件的 命名 一樣的時候,回呼函式不同就等於不一樣。
  • 若移除時不指定回呼函式,則 相同命名 的事件會一起被移除。
  • 若你想知道綁定了什麼事件,可以查看 _events 這個物件。
    • _events 物件中,同名事件會以陣列的方式堆疊。
    • 所以你可以故意修改堆疊順序( 小孩子不要學 )。

https://ithelp.ithome.com.tw/upload/images/20190916/20001433wa7zTvYVY8.png


小結

事件溝通在現今的 MVVM 生態系中,確實不是一個很容易的課題。不過由於 EventBus 帶來的方便性,也算是大幅度減低我們在操作上的麻煩。但,當你需要管理這些事件的時候,相對的也是挺惱人的。市面上有很多類似的小型專案或是套件可以使用,但說實在的我也不知道怎麼推薦,看起來大同小異,就挑自己喜歡的用吧。

最後,我們下一篇會提及 App 的溝通,應該算是溝通系列一個段落。在我們後續再次提及動態載入時,會再回來聊聊溝通的事情。


上一篇
Component 的溝通方式 Vuex Day 13
下一篇
Vue App 的溝通方式 Day 15
系列文
VueJS 從前端到後端31

尚未有邦友留言

立即登入留言