zepto源码研究 - event.js(底层api)

简要 :   event.js主要用于提供注册自定义事件和手动触发事件的功能。大概的实现方式是:首先开辟一个存放自定义事件的响应池并关联到元素的zId属性上。定义底层api来存取并执行响应池里面的方法。最后封装底层api,提供便捷的方法显式调用。包括注册(on,bind),移除(off,unbind),触发(trigger)等方法,其中对事件以及自定义的方法进行了进一步的封装。底层api源码如下:

var _zid = 1, undefined,
      slice = Array.prototype.slice,
      isFunction = $.isFunction,
      isString = function(obj){ return typeof obj == 'string' },
      handlers = {},//_zid: events    事件缓存池
      specialEvents={},
      focusinSupported = 'onfocusin' in window,      //是否支持即将获取焦点时触发函数   onfocusin focus不支持冒泡
      focus = { focus: 'focusin', blur: 'focusout' },    //焦点修正
      hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }    // mouseenter  mouseleave不冒泡的修正 ,mouseover mouseout功能一样且支持冒泡

  //此处标准浏览器,click、mousedown、mouseup、mousemove抛出的就是MouseEvents,应该也是对低版本IE等某些浏览器的修正
  specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'


  /**
   * 取元素标识符,没有设置一个返回
   * @param element
  * element._zid = _zid++;先赋值再++
* 作用:zid 对应于 事件缓存池的索引,可根据element的zid属性获取对应的自定义响应函数 * @returns {*|number}
*/ function zid(element) { return element._zid || (element._zid = _zid++) } /** * 根据形参条件 查找元素上事件响应函数集合 * @param element * @param event * @param fn * @param selector * @returns {Array}
*   
*/ function findHandlers(element, event, fn, selector) { //解析命名空间事件名 event = parse(event) // if (event.ns) var matcher = matcherFor(event.ns) //找到响应函数集合 return (handlers[zid(element)] || []).filter(function(handler) { return handler && (!event.e || handler.e == event.e) //若有event.e ,则判断事件类型是否相同,否则直接走下一步 && (!event.ns || matcher.test(handler.ns)) //若有event.e,则判断事件命名空间是否相同 RegExp.prototype.test = function(String) {}; && (!fn || zid(handler.fn) === zid(fn)) // zid(handler.fn)返回handler.fn的标识,没有加一个,判断fn标识符是否相同 && (!selector || handler.sel == selector) //若有selector 则判断selector是否相同 }) } /** * 解析事件类型 * @param event 'click' * @returns {{e: * 事件类型 , ns: string 命名空间}} */ //parse("click.zhutao.xiaoyu"); //Object {e: "click", ns: "xiaoyu zhutao"}
// 技巧:slice取从索引为1之后的所有项,sort对数组进行排序,join(" ")将数组变为字符串,中间插入空格
function parse(event) { //如果有.分隔,证明有命名空间 var parts = ('' + event).split('.') return {e: parts[0], ns: parts.slice(1).sort().join(' ')} } /** * 生成命名空间的正则对象 * @param ns * @returns {RegExp} */ /* * matcherFor("xiaoyu zhutao"); × 结果:/(?:^| ).* ?xiaoyu zhutao(?: |$)/ */ // 此用来生成事件命名空间的正则匹配,在findHandlers方法中有用到 function matcherFor(ns) { return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') } /** * 事件捕获 * 对focus和blur事件且浏览器不支持focusin focusout,通过设置捕获来模拟冒泡 * @param handler * @param captureSetting * @returns {*|boolean|boolean}
*/
  // addEventListener 的第三个参数, true - 事件句柄在捕获阶段执行, false - 默认。事件句柄在冒泡阶段执
  function eventCapture(handler, captureSetting) {
//如果是focus和blur事件且浏览器不支持focusin focusout时,
    //设置为可捕获,间接达到冒泡的目的
    return handler.del &&
        (!focusinSupported && (handler.e in focus)) ||
        !!captureSetting
  }

  /**
   *  修正事件类型 focus->focusIn blur->focusOut mouseenter->mouseover  mouseleave->mouseout
   * @param type   事件类型
   * @returns {*|boolean|*|*}
   */
  // 若用户输入的是focus,则实际注册的是focusIn 修正特定事件的类型
  function realEvent(type) {
    //hover[type] mouseenter和mouseleave 转换成   mouseover和mouseout
    // focus[type]  focus blur  修正为  focusin  focusout
    return hover[type] || (focusinSupported && focus[type]) || type
  }


  /**
   * 增加事件底层方法
   * @param element
   * @param events  字符串 如‘click'
   * @param fn
   * @param data
   * @param selector
   * @param delegator
   * @param capture
   */
  // add(element, event, callback, data, selector, delegator || autoRemove)
  function add(element, events, fn, data, selector, delegator, capture){
    //zid Zepto会在elemnt上扩展一个标识属性_zid
    // 读取元素上已绑定的事件处理函数
    var id = zid(element), set = (handlers[id] || (handlers[id] = []))

    // \s 匹配空格
    events.split(/\s/).forEach(function(event){
      //如果是ready事件
      if (event == 'ready') return $(document).ready(fn)

      //解析事件   {e: * 事件类型 , ns: string 命名空间}
      var handler   = parse(event)
      //保存fn,下面为了处理mouseenter, mouseleave时,对fn进行了修改

      //存储fn响应函数
      //存储selector
      handler.fn    = fn
      handler.sel   = selector

      // emulate mouseenter, mouseleave
      // 模仿 mouseenter, mouseleave

      //如果事件是mouseenter, mouseleave,模拟mouseover mouseout事件处理
      if (handler.e in hover) fn = function(e){
//          relatedTarget 事件属性返回与事件的目标节点相关的节点。
//            对于 mouseover 事件来说,该属性是鼠标指针移到目标节点上时所离开的那个节点。
//            对于 mouseout 事件来说,该属性是离开目标时,鼠标指针进入的节点。
//            对于其他类型的事件来说,这个属性没有用。
        var related = e.relatedTarget

        //不存在,表明不是mouseover、mouseout事件,
        //related !== this && !$.contains(this, related))  当related不在事件对象event内   表示事件已触发完成,不是在move过程中,需要执行响应函数
        if (!related || (related !== this && !$.contains(this, related)))
        //执行响应函数
          return handler.fn.apply(this, arguments)
      }

      //事件委托
      handler.del   = delegator
      var callback  = delegator || fn
      handler.proxy = function(e){
        //修正event
        // TODO 搞不清楚这里为什么要封装e
        e = compatible(e)

        //如果是阻止所有事件触发
        if (e.isImmediatePropagationStopped()) return
        e.data = data //缓存数据
        //执行回调函数,context:element,arguments:event,e._args(默认是undefind,trigger()时传递的参数)
        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))

        //当事件响应函数返回false时,阻止浏览器默认操作和冒泡
        if (result === false) e.preventDefault(), e.stopPropagation()
        return result
      }

      //设置事件响应函数的索引,删除事件时,根据它来删除  delete handlers[id][handler.i]
      handler.i = set.length

      //缓存到handlers[id]里    set = handlers[id]
      set.push(handler)

      //元素支持DOM2级事件绑定
      if ('addEventListener' in element)
      //绑定事件
      //DOM源码
//         @param {string} type
//        @param {EventListener|Function} listener
//        @param {boolean} [useCapture]     是否使用捕捉,默认 false
//        EventTarget.prototype.addEventListener = function(type,listener,useCapture) {};
      //realEvent(handler.e)  修正后的事件类型
      //handler.proxy 修正为代理上下文的事件响应函数
      // eventCapture(handler, capture)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  }

  /**
   *  删除事件,   对应add
   * @param element
   * @param events
   * @param fn
   * @param selector
   * @param capture  是否捕获
   */
  function remove(element, events, fn, selector, capture){
    var id = zid(element)  //找到元素标识
        ;(events || '').split(/\s/).forEach(function(event){ //events多个以空格分隔
      //遍历事件响应函数集合
      findHandlers(element, event, fn, selector).forEach(function(handler){
        delete handlers[id][handler.i]      //删除缓存在handlers的响应函数
        if ('removeEventListener' in element)
        //调用DOM原生方法删除事件
        //DOM源代码
//          /**
//           @param {string} type
//           @param {EventListener|Function} listener
//           @param {boolean} [useCapture]
//           */
//          EventTarget.prototype.removeEventListener = function(type,listener,useCapture) {};
        //realEvent(handler.e) 修正事件类型     handler.proxy  代理的事件响应函数     eventCapture(handler, capture)修正的是否捕获
        //与增加事件底层函数 add最后一行    element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))  呼应
          element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
      })
    })
  }

  //此处不清楚要干嘛,将事件两个核心底层方法封装到event对象里,方便做Zepto插件事件扩展?
  $.event = { add: add, remove: remove }

  /**
   * 代理
   * (function,context),(context,name)
   * @param fn
   * @param context
   * @returns {*}
   */
  // $.proxy是一个工厂,返回一个方法
  $.proxy = function(fn, context) {
    var args = (2 in arguments) && slice.call(arguments, 2)   //如果传了第3个参数,取到第3个参数以后(包含第3个参数)所有的参数数组,挺好的判断技巧
    if (isFunction(fn)) {   //fn是函数
      //采用闭包,以context调用函数。
      // args.concat(slice.call(arguments)) 将传参挪到前面  如传递给$.proxy(fn,context,3,4);  转变成  fn.apply(context,[3,4,fn,context,3,4])
      var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
      // 标记函数
      proxyFn._zid = zid(fn)
      return proxyFn
    } else if (isString(context)) {  //context是字符串, 实际传参(context,name)
      if (args) {                           //修正传参,再以$.proxy调用
        args.unshift(fn[context], fn)   // unshift  往数组开头添加新的项
        return $.proxy.apply(null, args)
      } else {
        return $.proxy(fn[context], fn)
      }
    } else {
      throw new TypeError("expected function")   //抛出异常:要求的函数类型错误
    }
  }



var returnTrue = function(){return true},
      returnFalse = function(){return false},
      ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/,//匹配 大写字母A-Z开头/returnValue/layerX/layerY用于createProxy(),过滤event对象的属性
      eventMethods = {
        preventDefault: 'isDefaultPrevented',//是否已调用preventDefault()    preventDefault      阻止浏览器的默认动作
        stopImmediatePropagation: 'isImmediatePropagationStopped', //是否已调用stopImmediatePropagation(),stopImmediatePropagation DOM3提出的阻止任何事件触发
        stopPropagation: 'isPropagationStopped' //是否已调用stopPropagation()  stopPropagation阻止冒泡
      }

  /**
   * 修正event对象
   * @param event   代理的event对象 原生event对象
   * @param source  原生event对象
   * @returns {*}
   */
  // 封装代理preventDefault  stopImmediatePropagation   stopPropagation等方法
  // 判断source.defaultPrevented 赋值event.isDefaultPrevented
  function compatible(event, source) {

    //event.isDefaultPrevented   是否已调用了preventDefault方法
    //

    //event是代理事件对象时,赋值给source
    if (source || !event.isDefaultPrevented) {
      source || (source = event)

      //遍历,代理preventDefault  stopImmediatePropagation   stopPropagation等方法 
//主要是为了判断事件是否已经被阻止默认和阻止冒泡等
$.each(eventMethods, function(name, predicate) { var sourceMethod = source[name] event[name] = function(){ //扩展event对象,代理preventDefault stopImmediatePropagation stopPropagation方法 ,兼容浏览器不支持,同时做其他事情 this[predicate] = returnTrue //如果执行了3方法,原生事件对象isDefaultPrevented isImmediatePropagationStopped isPropagationStopped 三方法标记true return sourceMethod && sourceMethod.apply(source, arguments) //且调用原生方法 } event[predicate] = returnFalse //扩展原生事件对象 isDefaultPrevented isImmediatePropagationStopped isPropagationStopped三方法,默认返回false。 }) //如果浏览器支持 defaultPrevented DOM3 EVENT提出的能否取消默认行为 //source.defaultPrevented : 判断默认事件是否已被阻止,与 preventDefault() 相对应
// 这是对各种情况的兼容 if (source.defaultPrevented !== undefined ? source.defaultPrevented : 'returnValue' in source ? source.returnValue === false : source.getPreventDefault && source.getPreventDefault()) event.isDefaultPrevented = returnTrue //默认可以取消 } //返回修正对象 return event } /** * 创建事件代理 * @param event Event对象 * @returns {*} */ // 新建一个对象 封装event,创建代理对象 function createProxy(event) { var key, proxy = { originalEvent: event } //存储原始event for (key in event) //复制event属性至proxy,ignoreProperties里包含的属性除外 if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] // return compatible(proxy, event) } /** * 创建Event对象 * @param type * @param props 扩展到Event对象上的属性 * @returns {*} * @constructor */ $.Event = function(type, props) { //当type是个对象时 保证type为对象的属性字符串,props为对象 if (!isString(type)) props = type, type = props.type //对应 specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' //创建event对象,如果是click,mousedown,mouseup mousemove,创建为MouseEvent对象,bubbles设为冒泡 //TODO: 为什么要把这些事件单独拎出来? var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true // (name == 'bubbles') ? (bubbles = !!props[name])如果是冒泡,确保是true/false 浏览器只识别true、false, !!props[name]明确进行类型转换 // event[name] = props[name] props属性扩展到event对象上 if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) //初始化event对象,type为事件类型,如click,bubbles为是否冒泡,第三个参数表示是否可以用preventDefault方法来取消默认操作 //初始化event对象,type:事件类型 如click bubbles能否 true: 能否使用preventDefault取消浏览器默认操作 //附上DOM源码 /* @browser Gecko @param {string} eventTypeArg @param {boolean} canBubbleArg @param {boolean} cancelableArg */ //Event.prototype.initEvent = function(eventTypeArg,canBubbleArg,cancelableArg) {}; event.initEvent(type, bubbles, true) //添加isDefaultPrevented方法,event.defaultPrevented返回一个布尔值,表明当前事件的默认动作是否被取消,也就是是否执行了 event.preventDefault()方法. return compatible(event) }

  
  


findHandlers: 

该函数的功能是根据形参找出满足条件的事件数组。技巧是利用了filter,并且根据形参的判断返回true和false,而且判断条件书写格式是值得借鉴的。这不同于一般的for,if ,else流程。

 

add(element, event, callback, data, selector, delegator || autoRemove):

add其实是将事件的封装对象加入到handlers列表中并addEventListener 注册事件。此方法在$.fn.on中运用到。

 

var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))    :    handler.proxy 封装了callback  ,使其执行上下文为element。

 

realEvent:

由于注册时传入的事件类型参数要区分focus等特殊类型,所以一般的流程是这样写:

if(type in special){

  addEventListener(special[type],......)

}else{

  addEventListener(type,......)

}

但这么做,代码重复了不说,还导致了整个结构的高耦合性,此方法的功能是将判断功能与注册功能分离开。这是zepto代码风格中很重要的一个技巧:就是功能模块尽可能分离。

 

$.Event:

该方法的第一句是判断参数类型,以便纠正各参数值。

var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true   :这一句有一个小技巧,createEvent 的形参分两种:‘mouseEvents’ 和 'Events' ,若是一般的流程,便是用if,else来分别createEvent,但这里将异类包装成一个对象,通过或运算输出想要的结果。减少了编写的代码量并且还很容易扩展(只需在specialEvents里添加对象,而不必关注行为出现在哪里)

 

 

 



 

posted @ 2016-07-23 20:28  潇洒-zhutao  阅读(402)  评论(0)    收藏  举报