Zepto源码分析——事件模块


Zepto的事件模块有些简化,因为Zepto作为一个针对移动端游览器的框架,所以游览器的事件系统本身相对完善,不需要做过多事件机制中最复杂的兼容处理。所以在分析后打算总结下针对PC端框架中兼容处理的。

游览器事件API

dom提供了三种层级的事件api:html事件、dom0级事件、dom2级事件(可以绑定多个回调)。整个事件机制主要简历在dom0级与dom2级两个标准上:http://bugzhang.com/2017/08/13/chang-yong-de-javascriptdai-ma-duan-2-dom-shi-jian-ajax-cookie/ ,改博文里第一段代码就是相关的实现,此处不再阐述。

与dom 0级事件的缺陷
  • 对于DOM3新增事件不支持,如:FocusIn,FocsuOut,MouseRemoved,MouseScrill等,但这些事件用的很少
  • 每次只可以绑定一个回调,重复绑定就会取消掉上次的绑定
  • 在ie下回调没有参数,在其他游览器回调第一个参数是事件对象
  • 只能再冒泡阶段可用
ie事件addachEvent的缺陷
  • this指向的是window,存在内存泄漏
  • 多钟时间绑定回调后,执行顺序不是按照绑定时的顺序触发
  • 与W3C有一些事件有区别
  • 只支持冒泡阶段
addEventListenner的缺陷
  • 部分游览器与标准的事件定义不一致
  • 第四个参数是ff跨文档监听事件,第五个参数是flash下制定监听函数的引用强弱
  • 事件对象不稳定,各个游览器有区别
  • input事件不如ie的propertychange事件好用

Zepto的Event模块核心方法

add:内部用来添加事件的方法
function add(element, events, fn, data, selector, delegator, capture) {
    //拓展标识属性zid
    var id = zid(element),
        set = (handlers[id] || (handlers[id] = []))
    events
        //匹配空格,获取多个event
        .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)
            //如果支持dom2级事件
            if ('addEventListener' in element)
                element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
        })
}
remove:内部删除事件的方法
function remove(element, events, fn, selector, capture) {
    var id = zid(element)
    eachEvent(events || '', fn, function(event, fn) {
        findHandlers(element, event, fn, selector).forEach(function(handler) {
            delete handlers[id][handler.i]
            element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
        })
    })
}

就是调用removeEventListener

on:添加事件

on是zepto中最核心的事件方法,源码如下:

  $.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){
      //如果是有one=true,先删掉事件,再执行事件
      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)
    })
  }

参数说明:

  • event:事件类型,可以通过空格的字符串方式添加("click mousedown"),或者事件类型为键,函数为值的方式({click:function,mousedown:function})。
  • selector:可选参数,事件委托的节点选择器
  • data:事件处理程序中的event.data属性
  • callback:事件处理程序的回调函数
  • one:绑定事件后,只触发一次回调

使用方式:

var elem = $('#content')
// observe all clicks inside #content:
elem.on('click', function(e){ ... })
// observe clicks inside navigation links in #content
elem.on('click', 'nav a', function(e){ ... })
// all clicks inside links in the document
$(document).on('click', 'a', function(e){ ... })
// disable following any navigation link on the page
$(document).on('click', 'nav a', false)

很多其他api是内部通过调用on方法实现的,如:

//基于一组特定的根元素为所有选择器匹配的元素附加一个处理事件,匹配的元素可能现在或将来才创建。
$.fn.delegate = function(selector, event, callback){
  return this.on(event, selector, callback)
}
//添加一个处理事件到元素,当第一次执行事件以后,该事件将自动解除绑定,保证处理函数在每个元素上最多执行一次。
$.fn.one = function(event, selector, data, callback){
  return this.on(event, selector, data, callback, 1)
}
//为一个元素绑定一个处理事件。
$.fn.bind = function(event, data, callback){
  return this.on(event, data, callback)
}
off:移除事件

off模块也是主要的核心模块,主要做移除事件

$.fn.off = function(event, selector, callback){
    var $this = this
    if (event && !isString(event)) {
        $.each(event, function(type, fn){
            $this.off(type, selector, fn)
        })
        return $this
    }

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

    if (callback === false) callback = returnFalse

    return $this.each(function(){
        remove(this, event, callback, selector)
    })
}

一些api是内部通过调用off方法实现的,如:

//移除Bind绑定的事件
$.fn.unbind = function (event, callback) {
    return this.off(event, callback)
}
//移除通过delegate 注册的事件。
$.fn.undelegate = function (selector, event, callback) {
    return this.off(event, selector, callback)
}
trigge与triggerHandler

在对象集合的元素上触发指定的事件

$.fn.trigger = function (event, args) {
    event = (isString(event) || $.isPlainObject(event))
        ? $.Event(event)
        : compatible(event)
    event._args = args
    return this.each(function () {
        // handle focus(), blur() by calling them directly
        if (event.type in focus && typeof this[event.type] == "function")
            this[event.type]()
        // items in the collection might not be DOM elements
        else if ('dispatchEvent' in this)
            this.dispatchEvent(event)
        else
            $(this).triggerHandler(event, args)
    })
}
$.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
}

triggle模拟整个冒泡过程,除了自身,还触发祖先节点与window的同类型的回调,在游览器底层上,使用的方法是dispatchEvent,如果是在ie上是fireEvent。

$.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)
}

底层采用的是游览器的createEvent和initEvent方法来模拟事件,从而实现自定义事件的功能。

一些补充

1.很多框架对于事件代理不采用捕获而采用冒泡的原因,是考虑兼容性
2.zepto因为自身主要做移动端的原因,所以对于低版本ie浏览器是不考虑的,我在上述中或多或少做了补充
3.zepto的很大一部分事件实际是在touch模块,这块主要是触摸事件,内容也比较多,打算留到下篇再写

发表评论

电子邮件地址不会被公开。 必填项已用*标注

😉😐😡😈🙂😯🙁🙄😛😳😮:mrgreen:😆💡😀👿😥😎😕