深入浅出Vue2自定义事件

一、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 需要有那些函数

  1. 我们需要注册事件,所以需要一个$on
  2. 我们需要关闭事件,所以需要一个$off
  3. 我们需要触发事件,所以需要以一个$emit
  4. 我们有时会注册一次性事件,所以需要一个$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)

  1. 判断events是不是数组,如果是数组遍历,然后调用$on注册
  2. 如果不是数组,直接注册
    • 判断 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)

  1. 判断有没有传递参数,如果没有传递参数,关闭组件上所有的自定义事件
  2. 判断events是否为数组,如果是数组,那么遍历继续调用$off
  3. 判断有没有注册事件,如果没有直接 return
  4. 判断有没有对应的 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()
  • 创建两个组件实例AB,我们测下在A上注册事件并且触发事件
  • AB组件通过全局事件总线进行通信。
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算法。
  • 我是小洛,一名正在奔跑的前端开发工程师,欢迎关注。
相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
Alo3656 小时前
面试考点复盘(二)
面试