一、Vue2自定义事件
相信大家都是用过Vue的自定义事件吧。比如全局事件总线eventBus
和给组件上定义自定义事件<xxx @eventName>
然后在自组件中使用emit
来触发事件,用起来很简单,但你有了解过他的原理嘛?今天让我们一起来探索下他的原理吧。
1.1 发布-订阅
- Vue2的自定义事件使用了发布订阅模式,举一个🌰哈。比如我在
<Header @search="handler"/>
这个组件上定义了一个search
事件,他会添加到组件的实例对象上的_events
这个属性上。他会被处理成下面这个形式。
javascript
const Header = {
_events: {
search:[handler]
}
}
当我们emit
这个事件的时候emit('search')
,他会调用handler
这个事件处理函数。
- 看上去很简单哈。但是如果让我们自己实现需要考虑什么问题呢?
1.2 需要有那些函数
- 我们需要注册事件,所以需要一个
$on
- 我们需要关闭事件,所以需要一个
$off
- 我们需要触发事件,所以需要以一个
$emit
- 我们有时会注册一次性事件,所以需要一个
$once
1.3 函数的参数
1.3.1 $on(event, fn)
- 注册事件的时候,可以注册单一事件,如
$on('defineEvent' ,() => {})
- 也可以注册多个事件,这多个事件绑定同一个事件处理函数,如
$on(['defineEvent1', 'defineEvent2'], () => {})
- 所以
event
可以是一个字符串也可以是一个数组,当是数组的时候,我们只需要循环遍历这个数组,给每一项都注册一次就好了。
1.3.2 $off(event, fn)
- 关闭事件也是同样的道理,可以关闭单一事件,如
$off('defineEvent' , fn)
- 也可以关闭多个事件,这多个事件绑定同一个事件处理函数,如
$off(['defineEvent1', 'defineEvent2'], fn)
- 所以
event
可以是一个字符串也可以是一个数组,当是数组的时候,我们只需要循环遍历这个数组,给每一项都调用一次$off
就好了。 - 还有一种情况,就是当我们
$off()
不传递任何参数的时候,这个时候我们需要将组件上的_events
重置为{}
,也就是关闭所有事件。 - 还有一种情况是我只传递事件名,不传递回调函数,那么这个时候我们应该删掉这个事件注册的所有回调。
1.3.3 $emit(event)
- 触发事件就比较简单啦。我们只需要传递一个事件名
event
就可以啦,还有额外要传递的参数,如果需要的话。
1.2.4 $once(event, fn)
- 一次性事件就比较简单了,只需要传递一个事件名和对应的回调
二、设计
2.1 $on、$off、$once、$emit
方法部署到哪里
- 上面我们分析了这个多,每个组件都需要去注册事件,所以都需要
$on、$off、$once、$emit
这四个方法,那么我们需要把这个方法部署到哪里呢? - 如果部署到每个组件的实例对象上,是不是有点浪费内存呢?
- 有没有一个地方,我可以只部署一次这4个方法,但是可以让每个组件都是使用这4个方法呢?
- 聪明的你或许已经想到答案了,我们可以使用继承,这个地方就是Vue的原型。
- 在Vue2中存在这样一个很重要的关系
Vue.prototype === VueComponent.prtotype.__proto__
,画一张图吧!
所以说我们可以把这四个方法添加到Vue.prototype上去
javascript
Vue.prototype.$on = function(event, fn) {}
Vue.prototype.$off = function(event, fn) {}
Vue.prototype.$once = function(event, fn) {}
Vue.prototype.$emit = function(event) {}
2.2 _events 应该以什么样的数据结构组织呢?
- 上面我们说
$on
一次可以注册多个事件 - 同样的道理,那么是不是一个事件也可以被注册多次呀。
- 如:
$on('defineEvent', fn1)
、$on('defineEvent', fn2)
- 很明显我们应该用数组·
javascript
const _events = {
defineEvent:[fn1, fn2]
}
当我们注册一个eventName
事件的时候,如果在_events
中不存在,那么我们添加这个eventName
属性,并将其赋值为[fn]
。
2.3 具体实现分析
2.3.1 $on(event, fn)
- 判断
events
是不是数组,如果是数组遍历,然后调用$on
注册 - 如果不是数组,直接注册
- 判断
vm._events[event]
是否存在,如果存在,直接push - 如果不存在,赋值为数组,再push
- 判断
javascript
Vue.prototype.$on = function(event, fn) {
const vm = this
if (Array.isArray(event)) {
event.forEach((item) => {
vm.$on(item, fn)
})
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
2.3.2 $once(event, fn)
- 我们只需要将
fn
包装一下,当事件被触发后,我们调用$off
关闭这个事件就可以了
javascript
Vue.prototype.$once = function(event, fn){
const vm = this;
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
2.3.3 $off(event, fn)
- 判断有没有传递参数,如果没有传递参数,关闭组件上所有的自定义事件
- 判断
events
是否为数组,如果是数组,那么遍历继续调用$off
- 判断有没有注册事件,如果没有直接
return
- 判断有没有对应的
fn
,const cbs = _event[event]
- 如果没有,那么注销这个事件注册的所有回调
- 如果有,那么遍历
cbs
找到与fn
相等的,然后从cbs
中删除
javascript
Vue.prototype.$off = function (event, fn) {
const vm = this;
if (!arguments) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
event.forEach((item) => {
vm.$off(item, fn)
})
}
// 去除所有的 cbs
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
let i = cbs.length;
while (i--) {
const cb = cbs[i]
// cb === cb.fn 对应一次性事件
if (cb === fn || cb === cb.fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
2.3.4 $emit(event)
- 根据
event
获取到对应是事件回到,然后遍历执行就可以了。
javascript
Vue.prototype.$once = function(event, fn){
const vm = this;
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
三、总体实现
- 上面我们已经实现了最重要的4个函数,下面我把完整版代码贴到下面哈。想学习的同学可以直接复制粘贴到本地进行调试。
3.1 最终实现代码
javascript
// Vue 构造函数
function Vue() {
this._events = {}
}
// 组件的构造函数
function VueComponent() {
this._events = {};
}
// 一个很重要的关系
VueComponent.prototype = Object.create(Vue.prototype)
// vm 实例
const vm = new Vue()
// 添加全局事件总线
Vue.prototype.$bus = vm
Vue.prototype.$on = function (event, fn) {
const vm = this
if (Array.isArray(event)) {
event.forEach((item) => {
vm.$on(item, fn)
})
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
Vue.prototype.$off = function (event, fn) {
const vm = this;
if (!arguments) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
event.forEach((item) => {
vm.$off(item, fn)
})
}
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
let i = cbs.length;
while (i--) {
const cb = cbs[i]
if (cb === fn || cb == cb.fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event) {
const vm = this;
const cbs = vm._events[event]
const args = [...arguments].splice(1);
if (cbs) {
for (let i = 0; i < cbs.length; i++) {
const cb = cbs[i]
cb.apply(vm, args)
}
}
return vm
}
Vue.prototype.$once = function(event, fn){
const vm = this;
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
3.2 测试
javascript
const A = new VueComponent()
const B = new VueComponent()
- 创建两个组件实例
A
、B
,我们测下在A
上注册事件并且触发事件 A
、B
组件通过全局事件总线进行通信。
javascript
A.$on('add', (a, b) => {
console.log(a+b)
})
A.$emit('add' ,1,2) // 输出3
// 兄弟之间进行通信
A.$bus.$on('define', (a,b)=> {
console.log(a,b)
})
B.$bus.$emit('define', 10,5) // 打印 10 5
- 以上就是Vue2的自定义事件原理。
- 接下来我们会自己实现一个Vue2的diff算法。
- 我是小洛,一名正在奔跑的
前端开发工程师
,欢迎关注。