企图给vuex补充事件触发,小心地实现一个发布订阅,并说服自己这是合理的。
主要内容:
- 跨组件通信:从event bus 到vuex
- 一个比较麻烦的case: 跨组件触发事件
- mutation和action,同步异步,动机拆解
- 封装发布订阅,让vuetool可调试追踪
- 完善代码,抽离逻辑,约定规范
作用:
- 可以方便地用
on
和emit
方式,从一个组件触发另一个组件方法,不需增加状态变量 - 可以在vuetool中追踪到事件触发的行为和时机
- 遵循vuex缘由,不加冗余逻辑,摆脱思想负担
- 不是标准,侧重好用
final code:
跨组件通信:从event bus 到vuex
为了维护单向数据流的清晰,vue(2.x以上)只支持$emit
和$on
进行父子组件通信。
$emit
)的状态。但在构筑多个业务组件时候,组件与组件的通信就会因层层传递等变得复杂。 vue官方为跨多层父子组件通信提供了两种方案: - 简单版本:
- 大型应用:
eventHub
的主要思想是通过一个新的vue实例,用它来集中处理组件中的通信:
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本身实现好的事件机制,完成了一套数据托管的流程。
vuex
, 并集成到 。 vuex采用 store模式
的思想解决数据追踪和调试的问题: // https://cn.vuejs.org/v2/guide/state-management.htmlvar 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
的函数体里面,我们就能增加调试代码,从而对每个修改数据的行为都能追踪定位。
一个比较麻烦的case: 跨组件触发事件
vuex很好地解决了大型跨组件通信问题,但有些情况使用起来会有些小纠结。比如下面:
// button.vue
假设button.vue
和List.vue
是分属比较远的两个组件,button想要触发List的事件,且无数据交互,即initAllData
方法严格属于List.vue
, 这种场景在vuex内如何实现?
// button.vue
但这样的实现总显得有点冗余,
- 首先,引入了无必要的状态变量
triggerInitData
- 加长了整个链路,引进了
watch
这种场景下我们更想要的其实是类似eventHub
的发布订阅模式,因为我们关注的是‘事件’而不是‘数据’。
- 同时存在vuex和一个‘eventHub’,是不是重复了两套逻辑
- 通过事件订阅的行为又面临不方便追踪调试的局面
针对第一点其实很好解,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 完成后调用,这时候我们打开, 选择第2个tab:vuex
, 可以看到,vuetool这类工具对每个mutation行为都进行了追踪。
mutation
很大作用是存储数据快照。 那action分发的意义又是?( ) - mutation 只能返回同步状态,如上述代码,如果
mutations[evt]
是异步函数,commit里面之后获取的data都是无意义的,此时真正的data还未返回 - 我们当然可以在commit里面以
.then
的方式书写调试逻辑,但这样就得约定所有mutation方法以promise方式书写,并且还牺牲了本身是同步状态的函数。 - 更好的做法是新增一个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方法的限制,更好地做法是把选择交给开发,并提出约定。
完善代码,抽离逻辑,约定规范
现在我们优化封装下我们的代码,考虑这两点:
- 这套事件机制可以直接打到vuex对象上
- 发布订阅这套逻辑可以抽取出来,在任意其他对象也可以使用
第一步为了main.js的清晰,我们应该把这小段逻辑抽离出来, 新建一个文件 vuex-events.js
, 我们的所有操作都是基于store对象的,所以要把store对象传递进去,main.js中可以这样调整:
import vuexEvent from './vuex-events'vuexEvent(store)复制代码
第二步,我们可以意识到发布订阅的逻辑在很多地方的实现都很一致,实现的最终效果通常为on
, off
, once
, emit
这样的方法,因此,我们也可以把这套逻辑抽离出来,并增加一个mixTo的方法,这样想为某个对象增加发布订阅功能的话,我们都可以采用类似events.mixTo(Object)
的方法,推荐参见的实现。
// eventsfunction 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.triggerEvents.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.jsimport 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中的跨组件触发事件问题,避免手写过多代码,但对于共享的数据,始终应该抽离到vuex state中,可以理解为我们在为应对组件间纯事件通信做一种尝试。