• home > webfront > ECMAS > javascript >

    原生js实现事件监听类on/emit/off方法,Vue event事件机制解读

    Author:zhoulujun Date:

    原生js封装事件绑定$on、事件触发$emit和事件移除$off,如何实现?事件分析:一对多,观察者模式。有事件队列,有事件触发,有事件移除。其实我们只需对每个事件维持一个队列的增减与调用即可。

    原生js封装事件绑定$on、事件触发$emit和事件移除$off,如何实现?

    事件分析:一对多,观察者模式

    1. 建立事件仓库:obj{key:val,key:val}

    2. 事件绑定on(name,fn):先判断事件名称name在事件仓库中是否存在,不存在,则初始化obj[name]=[];将需要绑定的事件push进数组中.

    3. 事件触发emit(name,val):先判断事件名称name在事件仓库中是否存在,存在则遍历数组每个fn元素,调用事件;val为传递的参数.

    4. 事件移除of(name,fn):先判断事件名称name在事件仓库中是否存在,再判断fn是否有,最后判断fn是否存在数组中,存在则删除fn数组元素;若无fn则清空数组.

    es6 代码如下

    class Event {
      constructor () {
        this.obj = {}
      }
    
      /**
       * 监听事件
       *    事件为一个对象,对应的时间名字为一个数组,当发送次事件是,一次执行数组存储的行数
       * @param name {string} 事件名称
       * @param fn  {function} 监听到事件后,执行函数
       */
      on (name, fn) {
        if (!this.obj[name]) {
          this.obj[name] = []
        }
        this.obj[name].push(fn)
        // 链式调用
        return this
      }
    
      // 移除事件
      off (name, fn) {
        if (!this.obj[name]) {
          return false
        }
        if (!fn) {
          this.obj[name] = []
        }
        if (!Array.isArray(this.obj[name]) || !this.obj[name].length) {
          return false
        }
        let index = this.obj[name].indexOf(fn)
        if (index === -1) {
          obj[name].length = 0;
          // 设长度为0比obj[name] = []更优,因为如果是空数组则又开辟了一个新空间,设长度为0则不必开辟新空间
          // this.obj[name] = []
          return false
        }
        this.obj[name].splice(index, 1)
        return this
      }
    
      // 发送事件
      emit (name, val) {
        if (!this.obj[name]) {
          return false
        }
        this.obj[name].map(fn => {
          fn(val)
        })
        return this
      }
    }
    
    let event = new Event()
    event.on('test', (val) => {
      console.log(val)
    })
    event.emit('test', 2)
    event.off('test')
    event.emit('test', 3)

    github 地址参考 https://github.com/zhoulujun/GoBang_Renju_es6_project/blob/master/src/games/EventListen.js


    Vue 实现事件 $on $off $emit 实现方式

    源码如下:node_modules/vue/src/core/instance/events.js

    /* @flow */
    
    import {
      tip,
      toArray,
      hyphenate,
      handleError,
      formatComponentName
    } from '../util/index'
    import { updateListeners } from '../vdom/helpers/index'
    
    export function initEvents (vm: Component) {
      在vue底下挂载一个 event 事件对象
      vm._events = Object.create(null)
      vm._hasHookEvent = false
      // init parent attached events
      const listeners = vm.$options._parentListeners
      if (listeners) {
        updateComponentListeners(vm, listeners)
      }
    }
    
    let target: any
    
    function add (event, fn) {
      target.$on(event, fn)
    }
    
    function remove (event, fn) {
      target.$off(event, fn)
    }
    
    function createOnceHandler (event, fn) {
      const _target = target
      return function onceHandler () {
        const res = fn.apply(null, arguments)
        if (res !== null) {
          _target.$off(event, onceHandler)
        }
      }
    }
    
    export function updateComponentListeners (
      vm: Component,
      listeners: Object,
      oldListeners: ?Object
    ) {
      target = vm
      updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
      target = undefined
    }
    
    export function eventsMixin (Vue: Class<Component>) {
      const hookRE = /^hook:/
      Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this
        // 如果对象是数组,遍历数据,执行$on 方法
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn)
          }
        } else {
          (vm._events[event] || (vm._events[event] = [])).push(fn)
          // optimize hook:event cost by using a boolean flag marked at registration
          // instead of a hash lookup
          if (hookRE.test(event)) {
            vm._hasHookEvent = true
          }
        }
        return vm
      }
    
      Vue.prototype.$once = function (event: string, fn: Function): Component {
        const vm: Component = this
        function on () {
          vm.$off(event, on)
          fn.apply(vm, arguments)
        }
        on.fn = fn
        vm.$on(event, on)
        return vm
      }
    
      Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
        const vm: Component = this
        // all
        if (!arguments.length) {
          vm._events = Object.create(null)
          return vm
        }
        // array of events
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            vm.$off(event[i], fn)
          }
          return vm
        }
        // specific event
        const cbs = vm._events[event]
        if (!cbs) {
          return vm
        }
        if (!fn) {
          vm._events[event] = null
          return vm
        }
        if (fn) {
          // specific handler
          let cb
          let i = cbs.length
          while (i--) {
            cb = cbs[i]
            if (cb === fn || cb.fn === fn) {
              cbs.splice(i, 1)
              break
            }
          }
        }
        return vm
      }
    
      Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        if (process.env.NODE_ENV !== 'production') {
          const lowerCaseEvent = event.toLowerCase()
          if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            tip(
              `Event "${lowerCaseEvent}" is emitted in component ` +
              `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
              `Note that HTML attributes are case-insensitive and you cannot use ` +
              `v-on to listen to camelCase events when using in-DOM templates. ` +
              `You should probably use "${hyphenate(event)}" instead of "${event}".`
            )
          }
        }
        let cbs = vm._events[event]
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
          for (let i = 0, l = cbs.length; i < l; i++) {
            try {
              cbs[i].apply(vm, args)
            } catch (e) {
              handleError(e, vm, `event handler for "${event}"`)
            }
          }
        }
        return vm
      }
    }

    代码解读方面,有人做了:Vue.js源码解读系列 - Vue的自定义事件机制 https://blog.seosiwei.com/detail/23


    其是这个就是个发布订阅模型

    发布订阅模型

    如果安装发布订阅模式来写,参考:

    观察者模式与发布订阅模式的区别 https://www.zhoulujun.cn/html/theory/engineering/model/9072.html







    转载本站文章《原生js实现事件监听类on/emit/off方法,Vue event事件机制解读》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2016_0624_8438.html