阅读 496

Zepto源码学习Event模块

为什么要看Zepto的源码,因为公司用的是这个。。。。
再看这个源码的过程中,因为对事件类型的不充分,导致学习起来有些费劲,所以在讲这个板块之前先对一些事件进行了解。

了解基本event信息

事件分发

下面是触发点击事件的代码,我们在inner上添加点击事件,在wrapper添加事件,点击inner都会触发click事件。但这种情况需要我们每次都去点击回调函数才会执行,有没有函数不需要我们手动触发,自动触发呢?

<div class="wrapper">
  wrapper
  <div class="inner">inner</div>
</div>
<script>
  var $inner = document.getElementById('inner');
  var $wrapper = document.getElementById('wrapper');

  $inner.addEventListener('click', function () {
    console.log(this.innerHTML);
  });

  $wrapper.addEventListener('click', function () {
    console.log(this.innerHTML);
  });
  </script>
复制代码

这里用到了一个需要用到一个API: createEvent,具体代码如下:

  let event = document.createEvent('Event')
  event.initEvent('click', true, true)
  $inner.dispatchEvent(event)
复制代码

这里我们通过createEvent创建了一个事件,并且其后必须马上进行初始化,然后通过dispatchEvent进行事件分发,这样就用js代码进行事件的触发,而不需要我们进行点击才能触发。

事件模拟

在event模块中有这么一段代码

focus = {focus: 'focusin', blur: 'focusout'},
hover = {mouseenter: 'mouseover', mouseleave: 'mouseout'}
复制代码

focus和blur我们都知道,但是为什么要重新隐射focusin和blur事件呢,在mdn中我们可以看到focus和focusin的区别在于focus不支持事件冒泡,如果不支持事件冒泡,那么带来的效果就是不能够进行事件委托。
同样的mouseenter和mouseleave也不支持事件冒泡,但是mouseenter会带来巨大的性能消耗,所以我们常用mouseover进行mouseenter进行事件的模拟。在鼠标事件中,有一个relatedTarget事件,在前面提到因为mouseover支持冒泡,那该如何来模拟mouseenter事件呢。relatedTarget事件属性返回的是和事件的目标节点相关的节点。对于mouseover事件来说,该属性是鼠标指针移到目标节点上所离开的那个节点。对于mouseout事件来说,该属性是离开目标时,鼠标进入的节点。根据上面的描述,我们可以对relatedTarget的值进行判断:如果值不是目标元素,也不是目标元素的子元素,就说明鼠标已经移入目标元素而不是在元素内部移动

核心代码

zid

var _zid = 1
function zid(element) {
  return element._zid || (element._zid = zid++)
}
复制代码

zid主要是用来标记已经绑定时间的元素,这个函数返回元素的_zid,如果没有,那就全局的zid加一,并且赋值给元素的_zid属性

parse

  function parse(event) {
    var parts=('' + event).split('.')
    return {
      e: parts[0], ns: parts.slice(1).sort().join(' ')
    }
  }
复制代码

parse方法用来分解事件名和命名空间,{e: 事件名, ns: 命名空间},先把event变成字符串进行分割,得到事件名,和命名空间,命名空间可以为s1.s2.s3这种

compatible

这是用来修正event对象中浏览器的差异

eventMethods = {
  preventDefault: 'isDefaultPrevented',
  stopImmediatePropagation: 'isImmediatePropagationStopped',
  stopPropagation: 'isPropagationStopped'
}
function compatible(event, source) {
  if (source || !event.isDefaultPrevented) {
    source || (source = event)

    $.each(eventMethods, function(name, predicate) {
      var sourceMethod = source[name]
      event[name] = function(){
        this[predicate] = returnTrue
        return sourceMethod && sourceMethod.apply(source, arguments)
      }
      event[predicate] = returnFalse
    })

    event.timeStamp || (event.timeStamp = Date.now())

    if (source.defaultPrevented !== undefined ? source.defaultPrevented :
        'returnValue' in source ? source.returnValue === false :
        source.getPreventDefault && source.getPreventDefault())
      event.isDefaultPrevented = returnTrue
  }
  return event
}
复制代码

具体来看看他的代码

if (source || !event.isDefaultPrevented) {
    source || (source = event)
复制代码

如果原事件存在,或者事件event的isDefaultPrevented为false或者不存在成立 如果原事件source不存在,就把event赋值给source

  $.each(eventMethod, function(name, predicate) {
    var sourceMethod = source[name]
    event[name] = function(){
      this[predicate] = returnTrue
      return sourceMethod && sourceMethod.apply(source, arguments)
    }
  }) 
复制代码

这里是遍历eventMethod,获取原事件对应的方法名sourceMethod。对event事件进行重新赋值,先把方法赋值为returnTrue函数,返回执行原方法的返回值。

event[predicate] = returnFalse
复制代码

新添加的属性初始化为returnFalse。

  event.timeStamp || (event.timeStamp = Date.now())
复制代码

看事件是否支持timeStamp,如果不支持,将Date.now()赋值给timeStamp,最后返回做了兼容性处理的event。

createProxy

function createProxy(event) {
  var key, proxy = { originalEvent: event }
  for (key in event)
    if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

  return compatible(proxy, event)
}
复制代码

这个函数的作用在于生成代理的event,首先在proxy的originalEvent挂载本身,然后遍历event,将event的属性复制到proxy,最后返回对proxy和event做兼容性处理。

add

// element 事件绑定的元素,events绑定的事件列表,fn事件执行时的句柄,data传递给事件对象的数据
// 绑定元素的选择器,delegator事件委托函数,capture哪个阶段执行事件句柄
function add(element, events, fn, data, selector, delegator, capture){
    var id = zid(element), set = (handlers[id] || (handlers[id] = []))
    events.split(/\s/).forEach(function(event){
      if (event == 'ready') return $(document).ready(fn)
      var handler   = parse(event)
      handler.fn    = fn
      handler.sel   = selector
      // emulate mouseenter, mouseleave
      if (handler.e in hover) fn = function(e){
        var related = e.relatedTarget
        if (!related || (related !== this && !$.contains(this, related)))
          return handler.fn.apply(this, arguments)
      }
      handler.del   = delegator
      var callback  = delegator || fn
      handler.proxy = function(e){
        e = compatible(e)
        if (e.isImmediatePropagationStopped()) return
        e.data = data
        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
        if (result === false) e.preventDefault(), e.stopPropagation()
        return result
      }
      handler.i = set.length
      set.push(handler)
      if ('addEventListener' in element)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  }
复制代码

add方法主要是给元素添加事件和事件响应。

id = zid(element), set = (handlers[id] || (handlers[id] = []))
复制代码

获取element的id,然后通过id来获取他的句柄容器

events.split(/\s/).forEach(function (event) {
  if (event == 'ready') return $(document).ready(fn)
})
复制代码

对events进行分解,如果event是ready就直接执行fn

var handler   = parse(event)
    handler.fn    = fn
    handler.sel   = selector
复制代码

对event进行事件名和命名空间进行分离,然后将信息挂载到handler上,handler的最终结构是这样的:

{
  fn: '', // 函数
  e: '', // 事件名
  ns: '', // 命名空间
  sel: '', // 选择器
  i: '',  // 函数索引
  del: '', // 委托函数
  proxy: '' // 代理函数
}
复制代码

继续看下面的

 if (handler.e in hover) {
   fn = function (e) {
     var related = e.relatedTarget;
     if (!related || (related !== this && !$.contains(this, related))) {
       return handler.fn.apply(this, arguments)
     }
   }
 }
复制代码

这就是我们最先提到的mouseover和mouseenter事件,这里就是对Hover事件进行判断,如果related不存在,或者related不等于目标元素,并且不是目标元素的子元素,就能够完成mouseenter的模拟,然后返回函数处理后的结果。

handler.proxy = function (e) {
  e = compatible(e);
  if (e.isImmediatePropagationStopped()) {
    return
  }
  e.data = data;
  var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
  if (result === false) {
    e.preventDefault();
    e.stopPropagation();
  }
  return result;
}
复制代码

首先对e进行兼容处理,然后判断是否阻止默认行为,如果是则啥都不做,把data挂载到event对象上,e是事件执行时的event对象,并且使用compatible对e进行修正。调用句柄,并且返回处理结果。

set.push(handler)
if ('addEventListener' in element) 
  element.addEventListener(realEvent(hander.e), handler.proxy, eventCapture(handler, capture))
复制代码

向句柄容器添加句柄,并且给元素添加事件。

on

$.fn.on = function (event, selector, data, callback, one) {
  var autoRemove, delegator, $this = this
  if (event && !isString(event)) {
    $.each(event, function (type, fn) {
      $this.on(type, selector, data, fn, one)
    })
    return $this
  }

  if (!isString(selector) && !isFunction(callback) && callback !== false)
    callback = data, data = selector, selector = undefined
  if (callback === undefined || data === false)
    callback = data, data = undefined

  if (callback === false) callback = returnFalse

  return $this.each(function (_, element) {
    if (one) autoRemove = function (e) {
      remove(element, e.type, callback)
      return callback.apply(this, arguments)
    }

    if (selector) delegator = function (e) {
      var evt, match = $(e.target).closest(selector, element).get(0)
      if (match && match !== element) {
        evt = $.extend(createProxy(e), {
          currentTarget: match,
          liveFired: element
        })
        return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
      }
    }
    }
    }

    add(element, event, callback, data, selector, delegator || autoRemove)
  })
}
复制代码

on方法是给元素绑定事件,最后调用的add方法。

 var autoRemove, delegator, $this = this
 if (event && !isString(event)) {
   $.each(event, function (type, fn) {
     $this.on(type, selector, data, fn, one)
   })
   return $this
 }
复制代码

如果event不是字符串,可能就是对象或者数组,然后对其遍历,每个都调用on函数。

  if (!isString(selector) && !isFunction(callback) && callback !== false)
      callback = data, data = selector, selector = undefined
    if (callback === undefined || data === false)
      callback = data, data = undefined
复制代码

这是针对参数不同的情况进行的操作

return $this.each(function (_, element) {
  if (one) 
  autoRemove = function (e) {
    remove(element, e.type, callback)
    return callback.apply(this, arguments)
  }
})
复制代码

如果one为true,autoRemove进行的操作是把元素上对应的事件进行解绑,并且调用回调。

if (selector) 
  delegator = function (e) {
    var evt, match = $(e.target).closet(selector, element).get(0)
    if (match && match !== element) {
      evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
      return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
    }
  }
  add(element, event, callback, data, selector, delegator || autoRemove)
复制代码

如果selector,就需要做事件代理,调用closet找到最近selector的元素。如果match存在,并且不是当前元素,就调用createProxy(),给当前事件对象创建代理对象,在调用extend方法,为其扩展currentTarget和liveFired属性,将代理元素和触发元素保存到事件对象中。 最后执行句柄函数,match作为上下文,用代理后的event对象evt替换掉原句柄函数的第一个函数。

triggerHandler

$.fn.triggerHandler = function (event, args) {
  var e, result;
  this.each(function(i, element) {
    e = createProxy(isString(event) ? $.Event(event) : event)
    e._args = args
    e.target = element
    $.each(findHandlers(element, event.type || event), function (i, handler) {
      result = handler.proxy(e);
      if (e.isImmediatePropagationStopped()) return false;
    })
  })
  return result;
}
复制代码

triggerHandler用于直接执行函数。

this.each(function(i, element) {
    e = createProxy(isString(event) ? $.Event(event) : event)
    e._args = args
    e.target = element
复制代码

遍历元素,然后判断event如果是字符串则使用$.Event创建事件,然后使用createProxy创建代理。

$.each(findHandlers(element, event.type || event), function (i, handler) {
      result = handler.proxy(e);
      if (e.isImmediatePropagationStopped()) return false;
    })
复制代码

寻找元素上所有的句柄,handler.proxy我们在之前提到过这是真的回调函数,如果有isImmediatePropagationStopped,则终止遍历。

Event

$.Event = function (type, props) {
  if (!isString(type)) props = type, type = props.type;
  var event = document.createEvent(specialEvents[type] || 'Events'),
    bubbles = true
  if (props)
    for (var name in props)(name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
  event.initEvent(type, bubbles, true)
  return compatible(event)
}
复制代码

简单来说这个部分就是创建事件,初始化事件,然后返回兼容后的event。

参考文章:
mouseenter与mouseover为何这般纠缠不清?
读Zepto源码之Event模块
Zepto文档

关注下面的标签,发现更多相似文章
评论