类名 common/base/Evented.js
import { defaultValue } from '../util'
import * as Util from '../util/EventUtil'
import { ProxyEvent } from './event/index'

/**
 * 事件基类,所有可接收或者发送的对象继承于此
 * @class Evented
 * @moduleEX BaseModule
 * @example
 *  * ```js
 * map.on('click', function(e) {
 * 	alert(e.latlng);
 * } );
 * ```
 * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
 *
 * ```js
 * function onClick(e) { ... }
 *
 * map.on('click', onClick);
 * map.off('click', onClick);
 * ```
 */
export default class Evented {
  constructor() {
    return this._createEventedProxy()
  }

  /**
   * @private
   * @description: 创建代理对象
   * @return {*}
   */
  _createEventedProxy() {
    this._decorateParents = []
    this._decorateProperties = {}
    const that = this
    return new Proxy(this, {
      set(target, key, value, receiver) {
        if (that._decorateProperties[key]) {
          const decorateProperty = that._decorateProperties[key]
          const { type, createInstance } = decorateProperty
          // 如果类型不一致,重新初始化构建对象
          if (!type || !(value instanceof type)) {
            value = createInstance(value)
          }
          // 如果属于事件基类且没有事件父级,设置事件父级
          if (
            value instanceof Evented &&
            Array.isArray(value._decorateParents) &&
            value._decorateParents.findIndex((v) => v.instance === that) === -1
          ) {
            value._decorateParents.push({
              instance: that,
              key
            })
          }
        }
        // 处理设置值重复情况,不触发事件
        if (target[key] === value) {
          return true
        }
        // set eventData响应式刷新内容
        const eventData = {
          target,
          key,
          value,
          oldValue: Reflect.get(target, key),
          receiver
        }
        const result = Reflect.set(target, key, value, receiver)
        // 私有变量不进行响应式更新
        if (/^_[^_].*/.test(key)) {
          return result
        }
        that.decorateEventFire('set', eventData)
        return result // 返回设置结果
      },
      get(target, key, receiver) {
        const result = Reflect.get(target, key, receiver)
        return result
      }
    })
  }

  decorateEventFire(eventName, eventData) {
    this._decorateEventFire(this, this, [], eventName, eventData, eventData)
  }

  /**
   * @private
   * @description: 发送响应式事件
   * @param {*} target 发送源
   * @param {*} sourceTarget 触发源
   * @param {*} firePath 事件路径
   * @param {*} eventName 事件名称
   * @param {*} eventData 事件主体
   * @param {*} sourceEventData 源事件主体
   * @return {*}
   */
  _decorateEventFire(
    target,
    sourceTarget,
    firePath,
    eventName,
    eventData,
    sourceEventData
  ) {
    const { key, value, oldValue } = eventData
    const eventPath = firePath.concat()
    eventPath.unshift(key)
    const currentEventData = {
      key,
      value,
      oldValue
    }
    // 响应式发送事件
    const proxyEvent = new ProxyEvent({
      eventName,
      target,
      eventData: currentEventData,
      eventPath: eventPath.concat(),
      sourceTarget,
      sourceEventData
    })
    target.fire(eventName, proxyEvent)
    // 向父节点传递
    const parents = target._decorateParents || []
    parents.forEach((detail) => {
      const { instance, key } = detail
      let currentKey = key
      if (instance && instance.type === 'collection') {
        currentKey = `${target.id}`
      }
      const parentEventData = {
        key: currentKey,
        value: instance[currentKey] || target,
        oldValue: instance[currentKey] || target
      }
      this._decorateEventFire(
        instance,
        sourceTarget,
        eventPath,
        eventName,
        parentEventData,
        sourceEventData
      )
    })
  }

  /**
   * @private
   * @description: 响应式属性传递方法定义
   * @param {String} key
   * @param {any} value
   * @param {Object} type
   * @param {Function} createInstance
   * @return {Object} 实体
   */
  _decorate(key, value, type, createInstance) {
    createInstance = defaultValue(createInstance, (v) => v)
    let instance = value
    if (!type || !(value instanceof type)) {
      instance = createInstance(defaultValue(value, {}))
    }
    this._decorateProperties[key] = {
      type,
      createInstance
    }
    return instance
  }

  /**
   * @function  Evented.prototype.on
   * @description 注册一个新的监听事件;<br/>
   * 示例如下:<br/>
   * <a href='#event1'>[1、注册一个事件]</a><br/>
   * <a href='#event2'>[2、一次注册多个事件 - 同一个回调函数]</a><br/>
   * <a href='#event3'>[3、一次注册多个事件 - 分别指回调应函数]</a><br/>
   * <a href='#event4'>[4、当types为字符串时 - 指定上下文]</a><br/>
   * <a href='#event5'>[5、当types为对象时 - 指定上下文]</a><br/>
   * @param {String | Object} [types = null] 事件类型<br/>
   * 当types为字符串时,可以定义单个或多个事件,单个事件:"click",多个事件:以空格分割:"click double-click";<br/>
   * 当types为对象时,使用如下方式指定事件:{'click': onClickFun, 'mouse-move': onMouseMoveFun}
   * @param {Function} [fn = null] 事件回调函数
   * @param {Object} [context = null] 事件回调函数的this关键字将指向的对象
   * @return {Object} 当前实例
   *
   * @example <caption><h5 id='event1'>注册一个事件</h5></caption>
   * // 初始化一个点击事件回调函数
   * const clickFunction = function (event) {
   *   console.log("点击事件:", event)
   * }
   * // 调用MapView或SceneView的on方法注册一个点击事件
   * view.on('click', clickFunction)
   *
   * @example <caption><h5 id='event2'>一次注册多个事件 - 同一个回调函数</h5></caption>
   * // 初始化一个事件回调函数
   * const eventFunction = function (event) {
   *   console.log("事件:", event)
   * }
   *
   * // 调用MapView或SceneView的on方法注册多个事件
   * // 多个事件类型使用同一个回调函数
   * view.on('click right-click-down', eventFunction)
   *
   * @example <caption><h5 id='event3'>一次注册多个事件 - 分别指回调应函数</h5></caption>
   * // 初始化一个左键点击事件回调函数
   * const clickFunction = function (event) {
   *   console.log("click事件:", event)
   * }
   *
   * // 初始化一个右键按下事件回调函数
   * const rightClickFunction = function (event) {
   *   console.log("right-click-down事件:", event)
   * }
   *
   * // 调用MapView或SceneView的on方法注册多个事件
   * // 每一个事件类型,使用单独的回调函数
   * // 注意使用此种方式,一种类型的事件仅能指定一个回调函数
   * view.on({
   *   "click": clickFunction,
   *   "right-click-down": rightClickFunction
   * })
   *
   * @example <caption><h5 id='event4'>指定上下文 - types类型为字符串</h5></caption>
   * // 初始化一个点击事件回调函数
   * const clickFunction = function (event) {
   *   console.log("点击事件:", event)
   *   console.log("上下文对象:", this)
   * }
   * // 调用MapView或SceneView的on方法注册一个点击事件
   * // 指定view为回调函数的上下文对象
   * view.on('click', clickFunction, view)
   *
   * @example <caption><h5 id='event5'>指定上下文 - types类型为对象</h5></caption>
   * // 初始化一个点击事件回调函数
   * const clickFunction = function (event) {
   *   console.log("点击事件:", event)
   *   console.log("上下文对象:", this)
   * }
   * // 调用MapView或SceneView的on方法注册一个点击事件
   * // 指定view为回调函数的上下文对象
   * view.on({
   *   "click": clickFunction,
   *   "right-click-down": clickFunction
   * }, view)
   */
  on(types, fn, context) {
    // types can be a map of types/handlers
    if (typeof types === 'object') {
      for (const type in types) {
        // we don't process space-separated Evented here for performance;
        // it's a hot path since Layer uses the on(obj) syntax
        this._on(type, types[type], fn)
      }
    } else {
      // types can be a string of space-separated words
      types = Util.splitWords(types)

      for (let i = 0, len = types.length; i < len; i++) {
        this._on(types[i], fn, context)
      }
    }

    return this
  }

  /**
   * @function  Evented.prototype.off
   * @description 移除事件<br/>
   * 示例如下:<br/>
   * <a href='#off1'>[1、移除一个事件的指定回调函数]</a><br/>
   * <a href='#off2'>[2、移除一个事件的所有回调函数]</a><br/>
   * <a href='#off3'>[3、移除多个事件的同一个指定的回调函数]</a><br/>
   * <a href='#off4'>[4、移除多个指定事件的回调函数]</a><br/>
   * <a href='#off5'>[5、删除时指定上下文 - types类型为字符串]</a><br/>
   * <a href='#off6'>[6、删除时指定上下文 - types类型为对象]</a><br/>
   * @param {string} [types] 移除指定事件类型上绑定的回调函数<br/>
   * 当类型为字符串时,可以移除单个或多个事件类型绑定的回调函数,单个事件:"click",多个事件:以空格分割:"click double-click";<br/>
   * 当types为对象时,使用如下方式移除事件:{'click': onClickFun, 'mouse-move': onMouseMoveFun}
   * @param {Function} [fn] 事件回调函数,当types为字符串,且不指定要删除的回调函数时,删除该事件上的所有回调函数
   * @param {Object} [context] 事件回调函数的this关键字将指向的对象
   * @return {Object} 当前实例
   *
   * @example <caption><h5 id='off1'>移除一个事件的指定回调函数</h5></caption>
   * // 一个事件的回调函数
   * const clickFunction = function (event) {
   *   console.log("点击事件:", event)
   * }
   * // 调用MapView或SceneView的off方法移除一个事件的回调函数
   * view.off('click', clickFunction)
   *
   * @example <caption><h5 id='off2'>移除一个事件的所有回调函数</h5></caption>
   * // 一个事件的回调函数1
   * const clickFunction1 = function (event) {
   *   console.log("点击事件1:", event)
   * }
   *
   * // 一个事件的回调函数2
   * const clickFunction2 = function (event) {
   *   console.log("点击事件2:", event)
   * }
   *
   * // 调用MapView或SceneView的off方法移除一个事件的所有回调函数
   * // 不指定回调函数,则移除该事件上的所有绑定的回调函数
   * view.off('click')
   *
   * @example <caption><h5 id='off3'>移除多个事件的同一个指定的回调函数</h5></caption>
   * // 多个事件的同一个回调函数
   * const eventFunction = function (event) {
   *   console.log("事件:", event)
   * }
   * // 调用MapView或SceneView的off方法移除多个事件的同一个指定的回调函数
   * view.off('click double-click', eventFunction)
   *
   * @example <caption><h5 id='off4'>移除多个指定事件的回调函数</h5></caption>
   * // 一个事件的回调函数
   * const clickFunction = function (event) {
   *   console.log("click事件:", event)
   * }
   * // 调用MapView或SceneView的off方法移除多个指定事件的回调函数
   * view.off({
   *    // 移除click事件上一个指定的函数
   *   "click": clickFunction,
   *   // 移除double-click上所有指定的函数
   *   "double-click": undefined
   * })
   *
   * @example <caption><h5 id='off5'>删除时指定上下文 - types类型为字符串</h5></caption>
   * // 一个事件的回调函数
   * const clickFunction = function (event) {
   *   console.log("点击事件:", event)
   * }
   * // 调用MapView或SceneView的off方法移除一个事件的回调函数
   * view.off('click', clickFunction, view)
   * // 调用MapView或SceneView的off方法移除一个事件的所有回调函数
   * view.off('click', undefined, view)
   *
   * @example <caption><h5 id='off6'>删除时指定上下文 - types类型为对象</h5></caption>
   * // 一个事件的回调函数
   * const clickFunction = function (event) {
   *   console.log("click事件:", event)
   * }
   * // 调用MapView或SceneView的off方法移除多个指定事件的回调函数
   * view.off({
   *    // 移除click事件上一个指定的函数
   *   "click": clickFunction,
   *   // 移除double-click上所有指定的函数
   *   "double-click": undefined
   * }, view)
   */
  off(types, fn, context) {
    if (!types) {
      // clear all listeners if called without arguments
      delete this._Evented
    } else if (typeof types === 'object') {
      for (const type in types) {
        this._off(type, types[type], fn)
      }
    } else {
      types = Util.splitWords(types)

      for (let i = 0, len = types.length; i < len; i++) {
        this._off(types[i], fn, context)
      }
    }

    return this
  }

  // attach listener (without syntactic sugar now)
  _on(type, fn, context) {
    this._Evented = this._Evented || {}

    /* get/init listeners for type */
    let typeListeners = this._Evented[type]
    if (!typeListeners) {
      typeListeners = []
      this._Evented[type] = typeListeners
    }

    if (context === this) {
      // Less memory footprint.
      context = undefined
    }
    const newListener = { fn, ctx: context }
    const listeners = typeListeners

    // check if fn already there
    for (let i = 0, len = listeners.length; i < len; i++) {
      if (listeners[i].fn === fn && listeners[i].ctx === context) {
        return
      }
    }

    listeners.push(newListener)
  }

  _off(type, fn, context) {
    let listeners
    let i
    let len

    if (!this._Evented) {
      return
    }

    listeners = this._Evented[type]

    if (!listeners) {
      return
    }

    if (!fn) {
      // Set all removed listeners to noop so they are not called if remove happens in fire
      for (i = 0, len = listeners.length; i < len; i++) {
        listeners[i].fn = Util.falseFn
      }
      // clear all listeners for a type if function isn't specified
      delete this._Evented[type]
      return
    }

    if (context === this) {
      context = undefined
    }

    if (listeners) {
      // find fn and remove it
      for (i = 0, len = listeners.length; i < len; i++) {
        const l = listeners[i]
        if (l.ctx !== context) {
          // eslint-disable-next-line no-continue
          continue
        }
        if (l.fn === fn) {
          // set the removed listener to noop so that's not called if remove happens in fire
          l.fn = Util.falseFn

          if (this._firingCount) {
            /* copy array in case Evented are being fired */
            // eslint-disable-next-line no-multi-assign
            this._Evented[type] = listeners = listeners.slice()
          }
          listeners.splice(i, 1)

          return
        }
      }
    }
  }

  /**
   * @function  Evented.prototype.fire
   * @private
   * @description: 激发指定类型的事件
   * @param {string} [type] 事件类型
   * @param {Object} [data] 待传递的数据
   * @param {Boolean} [propagate] 是否传播到父级
   * @return {Object} 当前实例
   */
  fire(type, data, propagate) {
    if (!this.listens(type, propagate)) {
      return this
    }

    const event = data

    if (event) {
      event.type = type
      event.target = this
      event.sourceTarget = (data && data.sourceTarget) || this
    }

    if (this._Evented) {
      const listeners = this._Evented[type]

      if (listeners) {
        this._firingCount = this._firingCount + 1 || 1
        for (let i = 0, len = listeners.length; i < len; i++) {
          const l = listeners[i]
          l.fn.call(l.ctx || this, event)
        }

        this._firingCount--
      }
    }

    if (propagate) {
      // propagate the event to parents (set with addEventParent)
      this._propagateEvent(event)
    }
    return this
  }

  /**
   * @function  Evented.prototype.listens
   * @private
   * @description: 验证是否监听此事件
   * @param {string} [type] 事件类型
   * @param {Boolean} [propagate] 验证父级监听此事件
   * @return {Boolean} 当前实例
   */
  listens(type, propagate) {
    const listeners = this._Evented && this._Evented[type]
    if (listeners && listeners.length) {
      return true
    }

    if (propagate) {
      // also check parents for listeners if event propagates
      for (const id in this._eventParents) {
        if (this._eventParents[id].listens(type, propagate)) {
          return true
        }
      }
    }
    return false
  }

  /**
   * @function  Evented.prototype.once
   * @private
   * @description: 事件监听,仅触发一次
   * @param {string} [types] 将侦听器函数(fn)添加到对象的特定事件类型中。您还可以传递几个空格分隔的类型(例如“click dblclick”),也可以传入例如 {click: onClick, mousemove: onMouseMove}。
   * @param {Function} [fn] 侦听器函数
   * @param {Object} [context] 指定侦听器的上下文(this关键字将指向的对象)
   * @return {Object} 当前实例
   */
  once(types, fn, context) {
    const self = this
    if (typeof types === 'object') {
      for (const type in types) {
        this.once(type, types[type], fn)
      }
      return this
    }

    const handler = Util.bind(function () {
      self.off(types, fn, context).off(types, handler, context)
    }, this)

    // add a listener that's executed once and removed after that
    return this.on(types, fn, context).on(types, handler, context)
  }

  /**
   * @private
   * @function  Evented.prototype.addEventParent
   * @description: 添加事件父级-将接收传播事件的Evented
   * @param {Evented} [obj] 事件父级对象
   * @return {Object} 当前实例
   */
  addEventParent(obj) {
    this._eventParents = this._eventParents || {}
    this._eventParents[Util.stamp(obj)] = obj
    return this
  }

  /**
   * @function  Evented.prototype.removeEventParent
   * @private
   * @description: 移除事件父级-将接收传播事件的Evented
   * @param {Evented} [obj] 事件父级对象
   * @return {Object} 当前实例
   */
  removeEventParent(obj) {
    if (this._eventParents) {
      delete this._eventParents[Util.stamp(obj)]
    }
    return this
  }

  _propagateEvent(e) {
    for (const id in this._eventParents) {
      this._eventParents[id].fire(
        e.type,
        Util.extend(
          {
            layer: e.target,
            propagatedFrom: e.target
          },
          e
        ),
        true
      )
    }
  }
}

// aliases; we should ditch those eventually

// @method addEventListener(…): this
// Alias to [`on(…)`](#evented-on)
Evented.addEventListener = Evented.on

// @method removeEventListener(…): this
// Alias to [`off(…)`](#evented-off)

// @method clearAllEventListeners(…): this
// Alias to [`off()`](#evented-off)
// eslint-disable-next-line no-multi-assign
Evented.removeEventListener = Evented.clearAllEventListeners = Evented.off

// @method addOneTimeEventListener(…): this
// Alias to [`once(…)`](#evented-once)
Evented.addOneTimeEventListener = Evented.once

// @method fireEvent(…): this
// Alias to [`fire(…)`](#evented-fire)
Evented.fireEvent = Evented.fire

// @method hasEventListeners(…): Boolean
// Alias to [`listens(…)`](#evented-listens)
Evented.hasEventListeners = Evented.listens
构造函数
成员变量
方法
事件