zoukankan      html  css  js  c++  java
  • Zepto事件模块源码分析

    Zepto事件模块源码分析

    一、保存事件数据的handlers

    我们知道js原生api中要移除事件,需要传入绑定时的回调函数。而Zepto则可以不传入回调函数,直接移除对应类型的所有事件。原因就在于Zepto在绑定事件时,会把相关的数据都保存到handlers对象中,因此就可以在这个变量中查找对应事件的回调函数,来移除事件。

    handlers对象的数据格式如下:

    {
      1: [ // handlers的值为DOM元素的_zid
        {
          del: function() {}, // 实现事件代理的函数
          e: "click", // 事件名称
          fn: function() {}, // 用户传入的回调函数
          i: 0, // 该对象在数组里的下标
          ns: "", // 事件的命名空间,只用使用$.fn.triggerHandler时可用,$.fn.trigger不能使用。
          proxy: function(e) {}, // 真正绑定事件时的回调函数,里面判断调用del或者fn
          sel: undefined // 要进行事件代理时传入的selector
        }
      ]
    }
    

    二、绑定事件

    主要流程图

    流程说明

    处理参数实现函数重载

    实现函数重载的重点就是判断参数的类型,处理参数:

    // 处理参数,实现函数重载
    if (!isString(selector) && !isFunction(callback) && callback !== false) // 没有传入selector参数
    	callback = data, data = selector, selector = undefined
    if (callback === undefined || data === false) // 没有传入data参数
    	callback = data, data = undefined
    if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替
    

    构建事件代理函数

    事件代理函数的重点为:在触发元素和e.target之间找到和参数selector相匹配的元素,生成事件对象,触发回调函数:

    // 如果有传入selector参数,构建代理函数
    if (selector) delegator = function(e){
      var evt, match = $(e.target).closest(selector, element).get(0) // match为e.target到触发元素范围内和selector相匹配的元素
    
      if (match && match !== element) { // 如果有相匹配的元素,改写事件对象,触发回调函数
        evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) 
        return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
      }
    }
    

    保存事件的相关信息

    保存事件的相关信息就是根据参数组成handler对象,保存到上面提过的handlers对象中。需要注意的是Zepto是通过DOM对象中添加一个_zid来连接DOM对象和对应的handler对象。

    使用zid函数来赋值和获取_zid:

    function zid(element) {
    	return element._zid || (element._zid = _zid++)
    }
    

    通过一个_zid而不是通过DOM对象的引用来连接handler是因为:防止移除掉DOM元素后,handlers对象还保存着对这个DOM元素的引用。通过使用_zid就可以防止这种情况发生,避免了内存泄漏。

    构建真正的回调函数proxy

    Zepto对事件对象进行了扩展,例如添加isImmediatePropagationStopped函数等,所以就要构建proxy函数来进行一层代理,改变触发时的事件对象。此外,如果用户需要进行事件代理,proxy函数执行时就会调用上面构建好的代理函数,否则调用用户传进来的回调函数。因此使用addEventListener所传入的真正回调函数就是proxy函数。

    focus和blur事件的冒泡

    focus和blur事件本身是不冒泡的,如果需要对这两个事件进行事件代理,就要运用一些小技巧。首先,如果浏览器支持focusin和focusout,就使用这两个可以冒泡事件来代替。如果浏览器不支持focusion和focusout,就利用focus和blur捕获不冒泡的特性,传入addEventListener中的第三个参数设置true,以此来进行事件代理。

    三、取消绑定事件

    取消绑定是比较简单的,由于事件绑定时都把相应的数据都保存到了handlers对象上,所以只要根据参数在这个对象里寻找对应的回调函数,使用removeEventListener取消绑定就可以了。

    四、触发事件

    触发事件也比较简单,通过document.createEvent来创建事件对象,然后调用dispatchEvent来触发事件。这里有点小优化就是focus和blur事件的触发直接调用focus()和blur()。

    创建事件的代码:

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

    上面有点要注意的就是当创建鼠标相关的事件时要在document.createEvent的第一个参数中传入’MouseEvents‘,以提供更多的事件属性。鼠标相关的事件指的是:click、mousedown、mouseup和mousemove。

    五、其他

    源码大概只有300多行,中其实还有很多值得我们学习的地方,所以大家大可以花点时间阅读一下。

    六、event模块源代码

    //     Zepto.js
    //     (c) 2010-2016 Thomas Fuchs
    //     Zepto.js may be freely distributed under the MIT license.
    
    ;(function($){
      /*
      _zid            :用来生成标示元素和回调函数的id,每标示一个就+1
      slice           :Array.prototype.slice
    
      handlers        :保存着所有的handler,结构如下,handlers对象的值为对应元素的_zid,值为对象数组
      {
        1: [ // handlers的值为DOM元素的_zid
          {
            del: function() {}, // 实现事件代理的函数
            e: "click", // 事件名称
            fn: function() {}, // 用户传入的回调函数
            i: 0, // 该对象在数组里的下标
            ns: "", // 事件的命名空间,只用使用$.fn.triggerHandler时可用,$.fn.trigger不能使用。
            proxy: function(e) {}, // 真正绑定事件时的回调函数,里面判断调用del或者fn
            sel: undefined // 要进行事件代理时传入的selector
          }
        ]
      }
    
      specialEvents   :生成一个模拟事件时,click、mousedown、mouseup和mousemove时使用'MouseEvents'参数
      focusinSupported:是否支持focusin的Boolean值
      focus           :浏览器支持focusin和focusout时,focus和blur就使用focusin和focusout来代替
      hover           :mouseenter和mouseout用mouseover和mouseout实现
       */
      var _zid = 1, undefined,
          slice = Array.prototype.slice,
          isFunction = $.isFunction,
          isString = function(obj){ return typeof obj == 'string' },
          handlers = {},
          specialEvents={},
          focusinSupported = 'onfocusin' in window,
          focus = { focus: 'focusin', blur: 'focusout' },
          hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
    
      specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
    
      // 获取元素或者函数的_zid,没有的话就生成一个。赋值给元素或者函数的_zid属性
      function zid(element) {
        return element._zid || (element._zid = _zid++)
      }
    
      // 根据给定的参数在handlers变量中寻找对应的handler
      function findHandlers(element, event, fn, selector) {
        event = parse(event) // 解析event参数,分离出事件名和ns
        if (event.ns) var matcher = matcherFor(event.ns) // 
        // 取出所有属于element的handler,并且根据event、fn和selector参数进行筛选
        return (handlers[zid(element)] || []).filter(function(handler) {
          return handler
            && (!event.e  || handler.e == event.e) // 事件名不同的过滤掉
            && (!event.ns || matcher.test(handler.ns)) // 命名空间不同的过滤掉
            && (!fn       || zid(handler.fn) === zid(fn)) // 回调函数不同的过滤掉(通过_zid属性判断是否同一个函数)
            && (!selector || handler.sel == selector) // selector不同的过滤掉
        })
      }
    
      // 解析event参数: "click.abc" -> {e: "click", ns: "abc"}
      function parse(event) {
        var parts = ('' + event).split('.')
        return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
      }
    
      // 生成匹配的namespace表达式:'abc def' -> /(?:^| )abc .* ?def(?: |$)/
      function matcherFor(ns) {
        return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
      }
    
      // 获取绑定事件时,指定是否冒泡或捕获阶段的Boolean值
      // 需要事件代理和浏览器不支持focusin并且是focus和blur事件时返回ture,即绑定在捕获阶段
      // (因为focus和blur不会冒泡,但是会捕获)
      function eventCapture(handler, captureSetting) {
        return handler.del &&
          (!focusinSupported && (handler.e in focus)) ||
          !!captureSetting
      }
    
      // 返回真正绑定的事件名,例如要绑定mouseenter事件,其实真正绑定的是mouseover事件来模拟mouseenter事件。
      function realEvent(type) {
        return hover[type] || (focusinSupported && focus[type]) || type
      }
    
      /**
       * 添加事件的实际方法
       * @param {Object}   element   DOM元素
       * @param {String}   events    事件字符串
       * @param {Function} fn        回调函数
       * @param {All}      data      绑定事件时传入的data,可以是各种类型   
       * @param {String}   selector  被代理元素的css选择器
       * @param {[type]}   delegator 进行事件代理的函数
       * @param {[type]}   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){
          // 如果事件名为ready,直接调用$.fn.ready方法
          if (event == 'ready') return $(document).ready(fn)
    
          // 构建handler
          var handler   = parse(event)
          handler.fn    = fn
          handler.sel   = selector
    
          // emulate mouseenter, mouseleave
          // mouseenter、mouseleave通过mouseover、mouseout来模拟。
          if (handler.e in hover) fn = function(e){
            // relatedTarget为相关元素,只有mouseover和mouseout事件才有
            // 对mouseover事件而言,相关元素就是那个失去光标的元素。对mouseout事件而言,相关元素则是获得光标的元素。
            var related = e.relatedTarget
            // 只当鼠标从元素外部移到元素内部才触发mouseenter,只当鼠标从元素内部移到元素外部才出发mouseleave。
            if (!related || (related !== this && !$.contains(this, related)))
              return handler.fn.apply(this, arguments)
          }
    
    
          handler.del   = delegator
          var callback  = delegator || fn // 需要进行事件代理时,调用的是封装了fn的delegator函数
    
          // 真正绑定事件时的回调函数
          // 通过该函数改写事件对象,为事件对象添加一些方法和属性,然后调用用户传进来的回调函数。并且使用户的回调函数返回false时禁止默认行为和禁止冒泡
          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 // 把handler在set中的下标赋值给handler.i
          set.push(handler) // 把handler保存起来
    
          // 最后绑定事件
          if ('addEventListener' in element)
            element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
        })
      }
    
      // 删除handler
      function remove(element, events, fn, selector, capture){
        var id = zid(element)
        ;(events || '').split(/s/).forEach(function(event){
          findHandlers(element, event, fn, selector).forEach(function(handler){
            delete handlers[id][handler.i]
          if ('removeEventListener' in element)
            element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
          })
        })
      }
    
      $.event = { add: add, remove: remove }
    
      $.proxy = function(fn, context) {
        var args = (2 in arguments) && slice.call(arguments, 2) // 第三个以及之后的参数
    
        // 参数fn为函数
        if (isFunction(fn)) {
          var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
          proxyFn._zid = zid(fn)
          return proxyFn
        // 第一个参数为对象的情况:$.proxy(context, "fnName" )
        } else if (isString(context)) {
          if (args) {
            args.unshift(fn[context], fn)
            return $.proxy.apply(null, args)
          } else {
            return $.proxy(fn[context], fn)
          }
        } else {
          throw new TypeError("expected function")
        }
      }
    
      $.fn.bind = function(event, data, callback){
        return this.on(event, data, callback)
      }
      $.fn.unbind = function(event, callback){
        return this.off(event, callback)
      }
      $.fn.one = function(event, selector, data, callback){
        return this.on(event, selector, data, callback, 1)
      }
    
      var returnTrue = function(){return true}, // 在compatible里有用到
          returnFalse = function(){return false}, // 在compatible里有用到
          // 构建事件对象时所不要的几个属性:returnValue、layerX和layerY(还有以大写字母开头的属性?)
          ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/, 
          // 事件对象需要添加的三个方法名
          eventMethods = {
            preventDefault: 'isDefaultPrevented',
            stopImmediatePropagation: 'isImmediatePropagationStopped',
            stopPropagation: 'isPropagationStopped'
          }
    
      // 添加eventMethods里面的三个方法:isDefaultPrevented、isDefaultPrevented和isPropagationStopped
      function compatible(event, source) {
        if (source || !event.isDefaultPrevented) {
          source || (source = event)
    
          // 通过改写原生的preventDefault、stopImmediatePropagation和stopPropagation方法实现
          $.each(eventMethods, function(name, predicate) {
            var sourceMethod = source[name]
            event[name] = function(){
              this[predicate] = returnTrue
              return sourceMethod && sourceMethod.apply(source, arguments)
            }
            event[predicate] = returnFalse
          })
    
          // 设置isDefaultPrevented默认指向的函数
          if (source.defaultPrevented !== undefined ? source.defaultPrevented : // 如果有defaultPrevented属性,就根据defaultPrevented的值来判断
              'returnValue' in source ? source.returnValue === false : // returnValue属性只在beforeunload事件中有用。而且用途是设置弹出框的文案,所以这行代码应该有问题,可以去掉
              source.getPreventDefault && source.getPreventDefault()) // getPreventDefault和defaultPrevented属性类似,不过是非标准的。为了兼容没有defaultPrevented参数的浏览器。
            event.isDefaultPrevented = returnTrue
        }
        return event
      }
    
      // 构建事件代理中的事件对象
      function createProxy(event) {
        var key, proxy = { originalEvent: event } // 新的事件对象有个originalEvent属性指向原对象
    
        // 将原生事件对象的属性复制给新对象,除了returnValue、layerX、layerY和值为undefined的属性
        // returnValue属性为beforeunload事件独有
        for (key in event)
          if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]
    
        // 添加eventMethods里面的几个方法,并返回新的事件对象
        return compatible(proxy, event) 
      }
    
      $.fn.delegate = function(selector, event, callback){
        return this.on(event, selector, callback)
      }
      $.fn.undelegate = function(selector, event, callback){
        return this.off(event, selector, callback)
      }
    
      $.fn.live = function(event, callback){
        // 通过document.body元素代理事件
        $(document.body).delegate(this.selector, event, callback)
        return this
      }
      $.fn.die = function(event, callback){
        // 移除掉document.body元素的相关代理事件
        $(document.body).undelegate(this.selector, event, callback)
        return this
      }
    
      // 只负责处理参数,绑定事件交由add函数处理
      $.fn.on = function(event, selector, data, callback, one){
        var autoRemove, delegator, $this = this
        // event参数为对象,回调自身进行批量绑定事件
        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) // 没有传入selector参数
          callback = data, data = selector, selector = undefined
        if (callback === undefined || data === false) // 没有传入data参数
          callback = data, data = undefined
        if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替
    
        // 给每一个Z对象里面的元素绑定事件
        return $this.each(function(_, element){
          // 如果有传入one参数,使用下面的autoRemove函数代替callback。执行一次后会自动移除该事件的绑定
          if (one) autoRemove = function(e){
            remove(element, e.type, callback)
            return callback.apply(this, arguments)
          }
    
          // 如果有传入selector参数,构建代理函数
          if (selector) delegator = function(e){
            var evt, match = $(e.target).closest(selector, element).get(0) // match为e.target到触发元素范围内和selector相匹配的元素
    
            if (match && match !== element) { // 如果有相匹配的元素,改写事件对象,触发回调函数
              evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) 
              return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
            }
          }
    
          // 通过add函数来绑定事件
          add(element, event, callback, data, selector, delegator || autoRemove)
        })
      }
    
      $.fn.off = function(event, selector, callback){
        var $this = this
        // event参数为对象,回调自身进行批量取消绑定事件
        if (event && !isString(event)) {
          $.each(event, function(type, fn){
            $this.off(type, selector, fn)
          })
          return $this
        }
    
        // 处理参数,实现函数重载
        if (!isString(selector) && !isFunction(callback) && callback !== false) // 没有传入selector参数
          callback = selector, selector = undefined
        if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替
    
    
        return $this.each(function(){
          remove(this, event, callback, selector)
        })
      }
    
      $.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
          // 通过直接调用focus()和blur()方法来触发对应事件,这算是对触发事件方法的一个优化
          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)
        })
      }
    
      // triggers event handlers on current element just as if an event occurred,
      // doesn't trigger an actual event, doesn't bubble
      // 直接触发事件的回调函数,而不是直接触发一个事件,所以也不冒泡
      $.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
      }
    
      // shortcut methods for `.bind(event, fn)` for each event type
      // 绑定和触发事件的快捷方式
      ;('focusin focusout focus blur load resize scroll unload click dblclick '+
      'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+
      'change select keydown keypress keyup error').split(' ').forEach(function(event) {
        $.fn[event] = function(callback) {
          return (0 in arguments) ?
            this.bind(event, callback) :
            this.trigger(event)
        }
      })
    
      // 生成一个模拟事件,如果是鼠标相关事件,document.createEvent传入的第一个参数为'MouseEvents',以提供更多的参数
      $.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)
      }
    
    })(Zepto)
    
  • 相关阅读:
    Kali渗透测试——UPNP网关发现工具Miranda
    Kali渗透测试——UPNP网关发现工具Miranda
    Kali渗透测试——EtherApe
    Kali渗透测试——EtherApe
    ValueError: Input 0 of node import/save/Assign was passed float from import/beta1_power:0 incompatib
    tensorflow C++:You must define TF_LIB_GTL_ALIGNED_CHAR_ARRAY for your compiler
    tensorflow C++手写数字识别
    使用C/C++编写Python扩展库
    软中断和硬中断
    系统调用和函数调用
  • 原文地址:https://www.cnblogs.com/oadaM92/p/5303137.html
Copyright © 2011-2022 走看看