企圖爲vuex添加發布訂閱:事件綁定和事件觸發

企圖給vuex補充事件觸發,當心地實現一個發佈訂閱,並說服本身這是合理的。
javascript

主要內容:html

  • 跨組件通訊:從event bus 到vuex
  • 一個比較麻煩的case: 跨組件觸發事件
  • mutation和action,同步異步,動機拆解
  • 封裝發佈訂閱,讓vuetool可調試追蹤
  • 完善代碼,抽離邏輯,約定規範

做用:

  • 能夠方便地用onemit方式,從一個組件觸發另外一個組件方法,不需增長狀態變量
  • 能夠在vuetool中追蹤到事件觸發的行爲和時機
  • 遵循vuex原因,不加冗餘邏輯,擺脫思想負擔
  • 不是標準,側重好用

final code:vuex-event.jsvue

跨組件通訊:從event bus 到vuex

爲了維護單向數據流的清晰,vue(2.x以上)只支持$emit$on進行父子組件通訊。
把一個組件想象成一個模塊,其實就是但願組件只維護一個輸入(prop)和一個輸出(事件$emit)的狀態。但在構築多個業務組件時候,組件與組件的通訊就會因層層傳遞等變得複雜。
vue官方爲跨多層父子組件通訊提供了兩種方案:java

  1. 簡單版本:eventHub
  2. 大型應用:vuex

eventHub的主要思想是經過一個新的vue實例,用它來集中處理組件中的通訊:git

var eventHub = new Vue()

eventHub.$on('add-todo', this.addTodo)
eventHub.$emit('add-todo', { text: this.newTodoText })
eventHub.$off('add-todo', this.addTodo)

複製代碼

這種方式簡單直接,將數據集中到新建立的vue實例中,同時利用vue自己實現好的事件機制,完成了一套數據託管的流程。
github

一個狀態管理
缺點是,這個vue實例內的數據以及事件觸發時機,在代碼開發還有調試階段,都不是足夠透明可維護的。當業務複雜數據狀態隨之增多時候,這種不足更明顯:觸發了一個事件後,咱們很難去追蹤這個過程隨之引起的數據變化;反過來,咱們也很難在數據視圖改變後,反向去追蹤是由哪一個事件引發的。
爲此,官方推薦了 vuex, 並集成到 vue-tool
vuex採用 store模式的思想解決數據追蹤和調試的問題:

// https://cn.vuejs.org/v2/guide/state-management.html
var store = {
  debug: true,
  state: {
    message: 'Hello!'
  },
  setMessageAction (newValue) {
    if (this.debug) console.log('setMessageAction triggered with', newValue)
    this.state.message = newValue
  },
  clearMessageAction () {
    if (this.debug) console.log('clearMessageAction triggered')
    this.state.message = ''
  }
}
複製代碼

這種模式下,能夠把數據集中到store對象中的state進行管理,同時,爲了方便對修改state這個行爲進行追蹤調試,vuex約定對state數據的修改都不能簡單地賦值,而是要通過一個提交方法commit(相似上面的setMessageAction),這樣在commit的函數體裏面,咱們就能增長調試代碼,從而對每一個修改數據的行爲都能追蹤定位。vuex

一個比較麻煩的case: 跨組件觸發事件

vuex很好地解決了大型跨組件通訊問題,但有些狀況使用起來會有些小糾結。好比下面:vue-cli

// button.vue
<button @click="initAllData">

// List.vue
<···v-for="item in list">
methods: {
    initAllData () {
        this.initData1()
        this.initData2()
        // dosomething else
    }
}

複製代碼

假設button.vueList.vue 是分屬比較遠的兩個組件,button想要觸發List的事件,且無數據交互,即initAllData方法嚴格屬於List.vue, 這種場景在vuex內如何實現?
一種作法是利用一個狀態來控制:api

// button.vue
<button @click="initAllData">
initAllData () {
    this.$store.commit('triggerInitData', true)
}
// List.vue
<···v-for="item in list">
// computed 引入triggerInitData
watch:{
    triggerInitData (val) {
        if (val) {
            this.initAllData()
            this.$store.commit('triggerInitData', false)
        }
    }
}
methods: {
    initAllData () {
        this.initData1()
        this.initData2()
        // dosomething else
    }
}
複製代碼

但這樣的實現總顯得有點冗餘,promise

  1. 首先,引入了無必要的狀態變量triggerInitData
  2. 加長了整個鏈路,引進了watch

這種場景下咱們更想要的實際上是相似eventHub的發佈訂閱模式,由於咱們關注的是‘事件’而不是‘數據’。
但若是所以咱們就引進一個全局發佈訂閱的話,缺點也很明顯:

  1. 同時存在vuex和一個‘eventHub’,是否是重複了兩套邏輯
  2. 經過事件訂閱的行爲又面臨不方便追蹤調試的局面

針對第一點其實很好解,eventHub將數據操做放到新實例上,經過事件機制完成通訊,但沒對數據作集中管理;vuex實現了,但vuex的核心在於數據中心,並不關心數據以外的,組件間方法的相互觸發。換言之事件綁定-事件觸發,以及vuex,更像是兩個解決方案,咱們應該在數據跨組件通訊時候使用vuex,在純事件類型上探索更好的方法。
因而這個問題如今的焦點在於,如何更好地在組件間觸發事件,使之對開發者透明,方便追蹤調試,同時不干擾正常的數據流?

mutation和action,同步異步,動機拆解

思考上面問題前,咱們先回過頭來看vuex的設計, 以及爲何有mutation和action
vuex爲了能在每次數據變化先後作跟蹤,創建了mutation,約定每次數據的修改都應該經過commit方法進行,咱們能夠把commit理解爲相似下面的實現

state: { data: 0 },
mutations: {
    setData (state, val) {
        state.data = val
    }
}
// 組件內調用
this.$store.commit('setData', 123)
// store.commit 相似實現
function commit (evt, val) {
    store.mutations[evt](store.state, data)
    console.log('檢測到commit以後變化': data)
}
複製代碼

查看vuex的api,也能夠發現,vuex對插件暴露的接口subscribe,也是在每一個 mutation 完成後調用,這時候咱們打開vue-tool, 選擇第2個tab:vuex, 能夠看到,vuetool這類工具對每一個mutation行爲都進行了追蹤。

因此, mutation很大做用是存儲數據快照。
那action分發的意義又是?( 尤在知乎的回答 )

  1. mutation 只能返回同步狀態,如上述代碼,若是mutations[evt]是異步函數,commit裏面以後獲取的data都是無心義的,此時真正的data還未返回
  2. 咱們固然能夠在commit裏面以.then的方式書寫調試邏輯,但這樣就得約定全部mutation方法以promise方式書寫,而且還犧牲了自己是同步狀態的函數。
  3. 更好的作法是新增一個action用來處理異步,確保mutation是同步,無論action什麼邏輯,只要最後觸發commit,提交mutation就好了,這樣數據的變化最終仍會通過mutation追蹤, 在諸如vuetool工具裏呈現.

從vuex的這些設計看來,很關注的一點是數據的可維護性,數據在進行變動時候,應該是可追蹤的。結合上一段的問題,若是想在組件間觸發事件,那最大的原則是不該該破壞數據在變動時候的可檢測性,都應該通過mutation層,在此基礎上,事件的行爲自己最好也能被追蹤記錄。

封裝發佈訂閱,讓vuetool可調試追蹤

確立了需求和原則後,咱們終於能夠優雅地寫代碼了,咱們整理下小目標:

1. vuex項目內,引進發布訂閱
2. 利用mutation, 使"事件觸發"這個行爲被記錄
3. 優化封裝代碼,約定和確保規範,使通訊過程無數據傳遞,以避免漏測數據流
複製代碼

咱們簡單快速實現下第1點, 在一個vue-cli2搭建起來的項目中,咱們直接在main.js中插入:

import store from './store'
···
store.$events = {}
store.$on = function (evt, fn) {
  store.$events['$' + evt] = fn
}
store.$off = function (evt) {
  store.$events['$' + evt] = null
}

store.$emit = function (evt, data) {
  if (!this.$events['$' + evt]) return
  this.$events['$' + evt](data)
}
// 綁定
// this.$store.$on('test', () => {
// console.log('test')
// })

// 調用
// this.$store.$emit('test')
···
複製代碼

這樣就有個簡單的雛形,也肯定了大概的調用方式,接着咱們思考第2點,如何讓這個事件行爲像mutation方法同樣能被檢測到。
一個最簡單的思路是,在$emit時候,咱們也提交一個mutation, 使行爲自己能經過mutation被記錄。結合vuex的動態加載模塊功能,咱們嘗試一下:

store.registerModule('myEvents', {
  mutations: {
    setEvent () {}
  }
})
store.$events = {}
store.$on = function (evt, fn) {
  store.$events['$' + evt] = fn
}
store.$off = function (evt) {
  store.$events['$' + evt] = null
}

store.$emit = function (evt, data) {
  if (!this.$events['$' + evt]) return
  this.$events['$' + evt](data)
  this.commit('setEvent', evt)  // 將事件evt當成payload提交給mutation
}
複製代碼

如今試下觸發一個事件,咱們在vuetool能夠看到,事件也被記錄下來了,而且payload就爲觸發的事件名:

這樣,咱們基本實現一套功能了,在繼續優化以前,惟獨針對第三條小目標思考下,倘若咱們嚴格限制 $emit參數的傳遞,防止不通過vuex的數據出現,那咱們應該這樣子寫:

store.$emit = function (evt) {
  if (!this.$events['$' + evt]) return
  this.$events['$' + evt]()
  this.commit('setEvent', evt)  // 將事件evt當成payload提交給mutation
}
複製代碼

倘若咱們傳遞的是組件間都公用的數據,是的,咱們應當抽取到vuex,而且在維護一個單純的事件觸發。但考慮到實際場景,咱們也有可能針對一個開關事件傳遞一個boolean, 或者根據操做類別返回一個選擇0,1,2之類。爲此,對emit方法的限制,更好地作法是把選擇交給開發,並提出約定。

完善代碼,抽離邏輯,約定規範

如今咱們優化封裝下咱們的代碼,考慮這兩點:

  1. 這套事件機制能夠直接打到vuex對象上
  2. 發佈訂閱這套邏輯能夠抽取出來,在任意其餘對象也可使用

第一步爲了main.js的清晰,咱們應該把這小段邏輯抽離出來, 新建一個文件 vuex-events.js, 咱們的全部操做都是基於store對象的,因此要把store對象傳遞進去,main.js中能夠這樣調整:

import vuexEvent from './vuex-events'
vuexEvent(store)
複製代碼

第二步,咱們能夠意識到發佈訂閱的邏輯在不少地方的實現都很一致,實現的最終效果一般爲on, off, once, emit這樣的方法,所以,咱們也能夠把這套邏輯抽離出來,並增長一個mixTo的方法,這樣想爲某個對象增長髮布訂閱功能的話,咱們均可以採用相似events.mixTo(Object)的方法,推薦參見events.js的實現。
咱們能夠簡單點實現:

// events
function Events () {}
Events.prototype.events = {}
Events.prototype.on = function (evt, callback) {
  if (!callback || !evt) return this
  this.events[evt] = this.events[evt] || []
  this.events[evt].push(callback)
  return this
}
Events.prototype.once = function (evt, callback) {
  let that = this
  let cb = function () {
    that.off(evt, cb)
    callback(arguments)
  }
  return this.on(evt, cb)
}
Events.prototype.off = function (evt, callback) {
  if (!evt) {
    return this
  }
  let events = this.events[evt]
  if (!callback) {
    delete this[evt]
  } else {
    for (let i = events.length; i--;) {
      if (events[i] === callback) {
        events.splice(i, 1)
        return this
      }
    }
  }
}
Events.prototype.trigger = function (evt, ...arg) {
  let events = this.events[evt]
  if (!evt || !events) return this
  let len = events.length
  for (let i = 0; i < len; i++) {
    events[i](...arg)
  }
}

Events.prototype.emit = Events.prototype.trigger
Events.mixTo = function (receiver) {
  var proto = Events.prototype
  if (isFunction(receiver)) {
    for (var key in proto) {
      if (proto.hasOwnProperty(key)) {
        receiver.prototype[key] = proto[key]
      }
    }
  } else {
    for (var key in proto) {
      if (proto.hasOwnProperty(key)) {
        receiver[key] = proto[key]
      }
    }
  }
}
function isFunction (func) {
  return Object.prototype.toString.call(func) === '[object Function]'
}
export default Events
複製代碼

而後vuex-event中引用

// vuex-events.js
import events from './events'
export default function (store) {
  events.mixTo(store)
  store.registerModule('myEvents', {
    mutations: {
      setEvent () {
      }
    }
  })
  console.log(store)
  store.$emit = function (evt, ...arg) {
    if (!this.events[evt]) return
    this.trigger(evt, ...arg)
    this.commit('setEvent', evt)
  }
}
複製代碼

最終的代碼:vuex-event.js。 時刻記得咱們是爲了解決在vuex中的跨組件觸發事件問題,避免手寫過多代碼,但對於共享的數據,始終應該抽離到vuex state中,能夠理解爲咱們在爲應對組件間純事件通訊作一種嘗試。

相關文章
相關標籤/搜索