深入浅出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算法。
  • 我是小洛,一名正在奔跑的前端开发工程师,欢迎关注。
相关推荐
哑巴语天雨21 分钟前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情34 分钟前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
乔峰不是张无忌3301 小时前
【HTML】动态闪烁圣诞树+雪花+音效
前端·javascript·html·圣诞树
Wyang_XXX1 小时前
CSS 选择器和优先级权重计算这么简单,你还没掌握?一篇文章让你轻松通关面试!(下)
面试
鸿蒙自习室1 小时前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
m0_748250741 小时前
高性能Web网关:OpenResty 基础讲解
前端·openresty
前端没钱2 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
NoneCoder2 小时前
CSS系列(29)-- Scroll Snap详解
前端·css
无言非影2 小时前
vtie项目中使用到了TailwindCSS,如何打包成一个单独的CSS文件(优化、压缩)
前端·css
我曾经是个程序员2 小时前
鸿蒙学习记录
开发语言·前端·javascript