企圖給vuex補充事件觸發,當心地實現一個發佈訂閱,並說服本身這是合理的。
javascript
主要內容:html
on
和 emit
方式,從一個組件觸發另外一個組件方法,不需增長狀態變量final code:vuex-event.jsvue
爲了維護單向數據流的清晰,vue(2.x以上)只支持$emit
和$on
進行父子組件通訊。
把一個組件想象成一個模塊,其實就是但願組件只維護一個輸入(prop)和一個輸出(事件$emit
)的狀態。但在構築多個業務組件時候,組件與組件的通訊就會因層層傳遞等變得複雜。
vue官方爲跨多層父子組件通訊提供了兩種方案:java
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
vuex
, 並集成到
vue-tool。
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
vuex很好地解決了大型跨組件通訊問題,但有些狀況使用起來會有些小糾結。好比下面:vue-cli
// button.vue
<button @click="initAllData">
// List.vue
<···v-for="item in list">
methods: {
initAllData () {
this.initData1()
this.initData2()
// dosomething else
}
}
複製代碼
假設button.vue
和List.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
triggerInitData
watch
這種場景下咱們更想要的實際上是相似eventHub
的發佈訂閱模式,由於咱們關注的是‘事件’而不是‘數據’。
但若是所以咱們就引進一個全局發佈訂閱的話,缺點也很明顯:
針對第一點其實很好解,eventHub將數據操做放到新實例上,經過事件機制完成通訊,但沒對數據作集中管理;vuex實現了,但vuex的核心在於數據中心,並不關心數據以外的,組件間方法的相互觸發。換言之事件綁定-事件觸發,以及vuex,更像是兩個解決方案,咱們應該在數據跨組件通訊時候使用vuex,在純事件類型上探索更好的方法。
因而這個問題如今的焦點在於,如何更好地在組件間觸發事件,使之對開發者透明,方便追蹤調試,同時不干擾正常的數據流?
思考上面問題前,咱們先回過頭來看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
很大做用是存儲數據快照。
mutations[evt]
是異步函數,commit裏面以後獲取的data都是無心義的,此時真正的data還未返回.then
的方式書寫調試邏輯,但這樣就得約定全部mutation方法以promise方式書寫,而且還犧牲了自己是同步狀態的函數。vuetool
工具裏呈現.從vuex的這些設計看來,很關注的一點是數據的可維護性,數據在進行變動時候,應該是可追蹤的。結合上一段的問題,若是想在組件間觸發事件,那最大的原則是不該該破壞數據在變動時候的可檢測性,都應該通過mutation層,在此基礎上,事件的行爲自己最好也能被追蹤記錄。
確立了需求和原則後,咱們終於能夠優雅地寫代碼了,咱們整理下小目標:
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方法的限制,更好地作法是把選擇交給開發,並提出約定。
如今咱們優化封裝下咱們的代碼,考慮這兩點:
第一步爲了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中,能夠理解爲咱們在爲應對組件間純事件通訊作一種嘗試。