资讯专栏INFORMATION COLUMN

Zepto源码学习Event模块

Jeff / 2397人阅读

摘要:再看这个源码的过程中,因为对事件类型的不充分,导致学习起来有些费劲,所以在讲这个板块之前先对一些事件进行了解。寻找元素上所有的句柄,我们在之前提到过这是真的回调函数,如果有,则终止遍历。参考文章与为何这般纠缠不清读源码之模块文档

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

了解基本event信息 事件分发

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

wrapper
inner

这里用到了一个需要用到一个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文档

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96580.html

相关文章

  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    spacewander 评论0 收藏0
  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    fuyi501 评论0 收藏0
  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    Julylovin 评论0 收藏0
  • Zepto源码之Touch模块

    摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。 大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swip...

    Prasanta 评论0 收藏0
  • zepto源码ajax模块学习

    摘要:对象待会讲,我认为是设计最巧妙的地方。在跨域的时候会使用到,这是为了禁止使用。的目的在于创建一个事件,然后在触发他,如果默认行为被取消了,则返回。这是的初始化,默认是请求,是新建的对象,表示浏览器是否应该被允许缓存响应。 在学习zepto的源码的时候,不得不称赞这些人的厉害,我虽然能看明白,但是要我写,估计吭哧吭哧写不出来。虽然现在很少人使用zepto了,但是学习这些源码我相信每次看都...

    hizengzeng 评论0 收藏0

发表评论

0条评论

Jeff

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<