一个普通的 Zepto 源码分析(三)

来源:互联网 发布:知满天孙景民 编辑:程序博客网 时间:2024/05/17 22:26

一个普通的 Zepto 源码分析(三) - event 模块

普通的路人,普通地瞧。分析时使用的是目前最新 1.2.0 版本。

Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块,以及 event 、 ajax 、 form 、 ie ,其中 event 模块也是比较重要的模块之一,我们可以借助它提供的方法实现事件的监听、自定义事件的派发等。最重要的是,做了一些事件的兼容,简化了我们的编码。

event 模块

这个模块代码行数要比 ajax 的少。还是老套路,对函数调用关系做个静态分析,结果得到一坨群魔乱舞的线条( ... )。

好在有一些函数可以直接略过。

  $.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)  }  $.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).delegate(this.selector, event, callback)    return this  }  $.fn.die = function(event, callback){    $(document.body).undelegate(this.selector, event, callback)    return this  }

除了 $.fn.one() 外,其余函数都已经废弃,可用 $.fn.on()$.fn.off() 代替。但是对$.fn.on() 统一后会不会违背单一职责原则呢?见仁见智,我认为会,开发效率代价有点高。

发布 / 订阅模式

简化 $.fn.on()$.fn.off() 后我们可以看到,它们的最终归宿分别是 add()remove() 闭包函数:

  $.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    }    ... // 传入参数的重载等处理    return $this.each(function(_, element){      if (one) autoRemove = function(e){...}      if (selector) delegator = function(e){...}      add(element, event, callback, data, selector, delegator || autoRemove)    })  }  $.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    }    ... // 传入参数的重载等处理    return $this.each(function(){      remove(this, event, callback, selector)    })  }

这里的 event 可以是一个由空格分割的事件类型字符串,也可以是一个可枚举的事件类型/回调 k-v 对象。所以一开头会在 $.each() 中对每个 k-v 调用自身。

先不管 add()remove() 内部是怎样的处理逻辑(多回调的处理等),可以看到最终都调用了浏览器方法:

  function add(element, events, fn, data, selector, delegator, capture){    ...      if ('addEventListener' in element)        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))    ...  }  function remove(element, events, fn, selector, capture){    ...      if ('removeEventListener' in element)        element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))    ...  }

利用了浏览器 BOM 的事件监听方法,维护了多个订阅者对事件的订阅。

而浏览器本身会产生事件(如用户操作时),它就是一个发布者。另外我们也有可自定义的发布者接口 $.fn.trigger()$.fn.triggerHandler() 用于发布自定义类型的事件,前者可通过 BOM 接口 dispatchEvent() 派发事件,后者通过内部的代理闭包来一次性地触发事件回调。

有人认为 Event 模块本身类似于代理者,但我认为不太恰当,它只简单提供了注册、解注册、下发事件的接口,没有明确的控制机制,即使是(后面提到的)事件命名空间也可看作不同的事件。实际上,无论是 jQuery 还是 Zepto 都是将window 对象(见 w3.org 的事件流图 )当作 event bus 的,每个 listener 只管订阅自己范围内的消息,事件的调度(派发、冒泡)是通过 DOM 事件流完成的,这当然是实打实的发布/订阅模式。

基于 event bus 的事件机制

上面我们说到是将 window 对象作为 event bus 来实现事件机制的。由于存在事件派发与冒泡机制,事件传播的路径形成了 DOM 事件流,而对于特定的事件目标,这条传播路径是唯一确定的,因为 DOM 树内一个节点只有唯一一个父节点。而从 w3.org 的事件流图 我们可以看到,一旦传播路径确定了,事件过程可分为 3 个阶段:捕获阶段、目标阶段、冒泡阶段。位于目标元素之上的祖先可以选择捕获和冒泡,也可以取消掉后续整个传播。当然啦,在目标阶段也可以取消掉目标元素的默认行为。

尽管事件的形成、传播、触发都是由浏览器完成的,但 addEventListener() 却是注册到事件目标上的。我们可以将事件流想象一条虚拟的支线:

Window 总线 -- targetA.click -- targetB.Event0 -- ... -->>                      |为 targetA 创造的事件流 -- Document -- ... -- parent -- targetA -- parent -- ... -->>                            ^                          ^  ^        ^                            |                          |  |        |                        Listener 捕获                 可能多个   Listener 冒泡                                                     Listener

当然啦,尽管逻辑上通过的事件流可能不一样,但实际的 Listener 还是挂在 DOM 树节点上不会变的。

compatible() 兼容修正函数

在开始具体分析之前,必须先搞清楚几个入度较高的函数。首当其冲的应该是 compatible() 函数了,它被 Event 模块内部的方法调用有 4 处,是模块内最高的了。由静态分析可知,大体有两种调用情况:

  // 假设 proxyEvent 是代理事件, nativeEvent 是原生事件  compatible(nativeEvent)  compatible(proxyEvent, nativeEvent)

而代理函数是这样的:

  var ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/  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[key] !== undefined 是 for...in 的常见操作,可以保证只继承具有有效值的属性。

其中这个 ignoreProperties 很奇怪,我翻到了 v1.0 的一个提交 18ba1f0 (增加了 [A-Z]|layer[XY]$ ),它是这么说的:

silence "layerX/Y" Webkit warnings for eventsThis means we can't just blindly extend all event properties onto theevent proxy object.

v1.1.0 的一个提交 b0eaeb7 (增加了 returnValue$ )则说 Chrome 废弃了该方法,即使是复制也会发出警告。

v1.1.7 的一个提交 c89e705 (增加了 webkitMovement[XY]$ )也说是为了消除 Chrome 的警告。

看来兼容性问题可以治愈强迫症(逃。至于浏览器做废弃检查和代码中先行检查哪个性能损耗代价大,我没比较过,直觉是前者大点,毕竟还要准备发警告的工作。

好的,现在来看看 compatible() 的具体实现:

  var returnTrue = function(){return true},      returnFalse = function(){return false},      /* 关注点 1 (被代理函数名对应断言表) */      eventMethods = {        preventDefault: 'isDefaultPrevented',        stopImmediatePropagation: 'isImmediatePropagationStopped',        stopPropagation: 'isPropagationStopped'      }  function compatible(event, source) {    if (source || !event.isDefaultPrevented) {      source || (source = event)      // 关注点 1 (被代理函数名对应断言表)      $.each(eventMethods, function(name, predicate) {        var sourceMethod = source[name]        event[name] = function(){          // 关注点 2 (设置条件桩函数)          this[predicate] = returnTrue          return sourceMethod && sourceMethod.apply(source, arguments)        }        event[predicate] = returnFalse      })      try {        event.timeStamp || (event.timeStamp = Date.now())      } catch (ignored) { }      // 关注点 3 (为支持跨平台,顺序:新浏览器、老式方法、非常早期的废弃 API )      if (source.defaultPrevented !== undefined ? source.defaultPrevented :          'returnValue' in source ? source.returnValue === false :          source.getPreventDefault && source.getPreventDefault())        event.isDefaultPrevented = returnTrue    }    return event  }

首先要保证是代理调用,或者 isDefaultPrevented 属性没有被设置过,否则无需处理直接返回。也就是说,很多人认为它会多重打包,其实并不存在,接着可以看到事件必定会被这个函数处理。

接着是对 3 个原生函数的一个代理封装,使得每次调用都会对相应的断言(作为函数名)设置一次条件桩函数,再调用回原来的函数。而默认的桩函数总是返回 false 代表对应方法还未被调用过。最后如果事件的默认动作已被取消,则相应条件桩应一直返回 true 。

另根据 4f3d4a8 在 safari 上 event.timeStamp 可能是只读的,只能忽略对时间戳的设置。

综上, compatible() 是一个兼容修正器,用来装饰上 Event 插件要提供的 3 个条件桩函数。

$.Event() 生成自定义事件

允许我们指定自定义的事件类型,创造一个事件对象,并将它触发(如 trigger() 等)。

  var specialEvents={}  specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = '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)  }

现在鼓励使用事件构造函数来 new 一个事件,如 new Event('xxx')new CustomEvent('xxx', {...})new MouseEvent('click', {...}) 等。但是早期的浏览器只支持 createEvent() 方法来创造事件,参数可以为UIEventsMouseEventsMutationEventsHTMLEvents 以及其他非标准事件等(如 Gecko 自己定义的事件类型)。有点夸张的是, initEvent() 已经被废弃了= = 唔,总之这里就是一些黑科技啦。

最后返回一个经过兼容修正的事件对象。

$.proxy 函数代理

该函数给传入的上下文环境或函数提供一层简单的代理,使得传入函数在调用的时候其 this 指针指向传入的上下文对象,其实这有点像 ES6 的bind() 函数了;或者有第二种形式,将函数赋给传入的上下文对象的一个属性,并传入该属性名。

  $.proxy = function(fn, context) {    var args = (2 in arguments) && slice.call(arguments, 2)    if (isFunction(fn)) {      var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }      // 关注点 1 (代理函数与原函数被视作同一回调)      proxyFn._zid = zid(fn)      return proxyFn    } else if (isString(context)) {      if (args) {        // 关注点 2 (简单重载)        args.unshift(fn[context], fn)        return $.proxy.apply(null, args)      } else {        return $.proxy(fn[context], fn)      }    } else {      throw new TypeError("expected function")    }  }

首先 proxyFn 是一个闭包函数。但是 proxyFn._zid = zid(fn) 这个操作有点奇怪。查找zid() 的引用发现后面有一个 zid(handler.fn) === zid(fn) 的判断。看来当作为事件回调函数时,会被认为同一个函数,也就是说在$.fn.off() 的时候只要传入原函数,即会解除代理函数的事件回调。即使是把原函数再代理一遍(比如多次更换上下文对象等),也会一并被找到并解除。

对于第二种调用形式,比较简单粗暴,算是一种重载吧。

深入 add() 函数

绑定回调句柄及其集合

先看两个直接调用的函数,我的意思是,一开始用于处理参数的函数,它们或许会很重要:

  var _zid = 1,      handlers = {}  function zid(element) {    return element._zid || (element._zid = _zid++)  }  function parse(event) {    var parts = ('' + event).split('.')    return {e: parts[0], ns: parts.slice(1).sort().join(' ')}  }  function add(element, events, fn, data, selector, delegator, capture){    // 关注点 1 (绑定回调句柄及其集合)    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)      ...      // 关注点 2 (记录进集合)      handler.i = set.length      set.push(handler)      ...    })  }

这里为原生对象绑定了一个自增的 _zid ,而不是绑定 Zepto 对象,因为每次 $() 拿到的封装对象都是新 new 出来的。另外 DOM 本身就是个巨大的多级表,直接给 DOM 中的元素添加属性就好了,反过来我们还可以利用这个 id 作为元素的索引。

绑定了 id 就要用,接下来看到 set = (handlers[id] || (handlers[id] = [])) 维护了一个事件句柄 handler 对象的集合。后面利用数组长度作为新加入对象的序号标记。

parse() 函数用于解析单个事件的命名空间,在 Zepto 的文档上没有提到,翻 jQuery 的 event.namespace 才找到说明,主要是根据不同命名空间,对同一事件执行不同响应用的。这时事件类型大概会长成这个样子(是由用户传入的):

    test.somethingA    ErrorEvent.otherthingB.orPluginC.orSubscriberD

模拟 mouseentermouseleave 事件

很多人认为模拟这两个事件是为了支持往祖先冒泡, emmm.. 或许吧,但我是支持不冒泡的,天知道为什么会有父元素要知道子元素被进入的需求。我认为更多还是兼容性的问题,早期浏览器是并不支持这两个事件的。

  var hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }  function add(element, events, fn, data, selector, delegator, capture){    ...      var handler   = parse(event)      handler.fn    = fn      handler.sel   = selector      // emulate mouseenter, mouseleave      if (handler.e in hover) fn = function(e){        // 关注点 1 (该属性的使用)        var related = e.relatedTarget        // 关注点 2 (包含性查找)        if (!related || (related !== this && !$.contains(this, related)))          return handler.fn.apply(this, arguments)      }    ...  }

handler.sel 会在查找 handler 的时候用到,这里先不管。

如何模拟?显然如果我们能知道在鼠标移动的时候,指针指向了哪个元素就好了,最多也就监控一下这个指向是否发生了变化而已。

万幸的是,我们有 .relatedTarget 只读属性。根据 MDN 上 MouseEvent.relatedTarget 的介绍,mouseover 事件的 relatedTarget 会指向“从哪里来”的元素,而 mouseout 事件的则会指向“到哪里去”的元素。以mouseover 事件为例,我们可能从外部进入,也可能从子元素(移出)进入,从子元素进入的事件会被冒泡上来,我们可以很好地用 $.contains() 判断这个子元素是在我们的事件目标元素之下的。

要注意的是 target 属性正好反过来,还是以 mouseover 事件为例,不管是从外部进入触发的,还是子元素冒泡上来的,其target 属性永远都是指向我们的事件目标元素,无法将二者区分开来。

事件委托与代理

我们需要看回原本 $.fn.on()delegator 参数中传了个什么样的委托进来。

  $.fn.on = function(event, selector, data, callback, one){    ...    return $this.each(function(_, element){      // 关注点 1 (自动解绑的委托)      if (one) autoRemove = function(e){        remove(element, e.type, callback)        return callback.apply(this, arguments)      }      // 关注点 1 (匹配选择符的委托)      if (selector) delegator = function(e){        // 关注点 2 (向上查找最近节点)        var evt, match = $(e.target).closest(selector, element).get(0)        if (match && match !== element) {          evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})          // 关注点 3          return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))        }      }      add(element, event, callback, data, selector, delegator || autoRemove)    })  }

autoRemove 很好理解,它是一个自动解绑的委托:当被调用的时候先把对应的一次性回调函数移除,然后执行它的历史使命。为什么要先移除?较大可能是因为回调函数是用户自定义的,如果出现未捕获的异常会中断代码执行,不能正常移除。

delegator 则是一个条件委托,只有当事件源元素符合给定的 CSS 选择器时,事件才能够被响应。当然由于冒泡的存在,冒泡路径上的元素都是事件源元素,所以每次都会从事件源开始往上查找匹配 CSS 选择器的第一个元素,直到超出给定的element 范围为止(即不是它的后代节点)。

接下来则会为事件创建代理,并添加两个属性,分别是符合目标的元素,以及激发事件的元素。然后是完成委托任务,如果 autoRemove 委托存在则交由它来执行,否则自行调用回调函数。最后传入add() 函数做进一步的处理~

现在再看回 add() 函数中的事件代理:

      handler.del   = delegator      var callback  = delegator || fn      handler.proxy = function(e){        e = compatible(e)        // 关注点 1        if (e.isImmediatePropagationStopped()) return        // 关注点 2        e.data = data        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))        // 关注点 3        if (result === false) e.preventDefault(), e.stopPropagation()        return result      }

由于 stopImmediatePropagation() 的效果不但是阻止事件冒泡,还阻止后面其他回调的响应,因此需要在代理中判断该函数有没有执行过,只有被允许执行,才会执行回调函数。而回调函数的执行结果也会影响后续冒泡。

注册监听

这里就比较简单了,只有两个微小的操作需要注意。

  // 关注点 2 (对 focusin / focusout 的兼容转换)  var focusinSupported = 'onfocusin' in window,      focus = { focus: 'focusin', blur: 'focusout' },      hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }  // 关注点 3 (伪兼容支持..)  function eventCapture(handler, captureSetting) {    return handler.del &&      (!focusinSupported && (handler.e in focus)) ||      !!captureSetting  }  // 关注点 2  function realEvent(type) {    return hover[type] || (focusinSupported && focus[type]) || type  }  function add(element, events, fn, data, selector, delegator, capture){    ...    events.split(/\s/).forEach(function(event){      ...      var handler   = parse(event)      ...      if (handler.e in hover) fn = function(e){...}      ...      handler.proxy = function(e){...}      ...      // 关注点 1      if ('addEventListener' in element)        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))    })  }

首先是一个兼容性的处理,对于 mouseentermouseout 的模拟前面已经探究过了,这里还有个问题是还有相当多的浏览器竟然不支持 focusin / focusout 事件,比如(主要是)直至今年一月份的 FF 。因此我们只能使用较老的事件类型名 focus / blur 。而realEvent() 就是在做事件类型名的转换工作。

此外,前者是在元素获得或失去焦点产生的,会冒泡;后者则是在焦点转移后才触发,并且不会冒泡。对于这一兼容问题,没有太好的模拟方案,只能在捕获阶段就触发,造成一种冒泡的假象。实际上触发的顺序还是向下传播的顺序,而stopPropagation() 会断掉整个传播路径,所以使用时要小心。这一操作体现在 eventCapture() 内。

最后有一个 captureSetting 的参数,找不到任何从用户传入的途径,可能是 Zepto 的 Event 模块并没有提供注册捕获阶段回调的接口。

深入 remove 函数

查找回调句柄对象

首先要看 $.fn.off() 函数的说明:

  1. 要传入与调用 $.fn.on() 时相同的函数;
  2. 如果只传入事件类型名,会解绑所有该类型的事件回调;
  3. 如果什么都不传,解绑当前元素的所有事件回调。

第一点或许有疑问,怎么知道是不是相同的函数呢?我们在上面知道每个元素会绑定一个 _zid ,该模块以每个 id 为索引维护了一个关于handler 的集合,而我们传入的回调函数绑定在 handler.fn 上。看来可以想办法找到这些 handler

  function zid(element) {    return element._zid || (element._zid = _zid++)  }  function findHandlers(element, event, fn, selector) {    event = parse(event)    // 关注点 1 (生成匹配命名空间的正则)    if (event.ns) var matcher = matcherFor(event.ns)    return (handlers[zid(element)] || []).filter(function(handler) {      // 关注点 3 (筛选符合条件的 handler )      return handler        && (!event.e  || handler.e == event.e)        && (!event.ns || matcher.test(handler.ns))        && (!fn       || zid(handler.fn) === zid(fn))        && (!selector || handler.sel == selector)    })  }  function parse(event) {    var parts = ('' + event).split('.')    // 关注点 2 (命名空间字典序)    return {e: parts[0], ns: parts.slice(1).sort().join(' ')}  }  function matcherFor(ns) {    // 关注点 1    return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')  }

由于有事件命名空间的存在,查找过程需要多费点劲。看起来 matcherFor() 就是干这个的。

我们知道拥有多个命名空间的事件可能长这样: event.nsF.nsA.nsC ,而最终 event.ns 则应该是nsA nsC nsF (按字典序)。如果我想匹配它怎么办呢?不好直接等于,因为像 nsA nsB nsC nsF 这样的跟它是包含关系。我们需要一个正则表达式把插在两边、插在中间的命名空间过滤掉。

过滤两边是很好办的,因为命名空间字符串是以空格分割的,这也是两个捕获组 (?:^| )(?: |$) 的由来。

过滤中间也比较好办,用一个非贪婪匹配掉任意字符就好了,也就是 .*? 的作用。

于是像 (?:^| )nsA .* ?nsC .* ?nsF(?: |$) 这样的正则可以匹配出下面这些:

  nsA nsC nsF  nsA nsB nsC nsF  ns0 nsA nsB nsC nsF  ns9 nsA nsB nsC nsD nsE nsF nsZ

不过很明显.. Zepto 又写错了.replace(' ', ' .* ?') 只会匹配替换第一次出现的空格,不满足需求。正确的替换应该是.replace(/ /g, ' .* ?')

之后的过程就比较简单啦,拿到 handlers[_zid] 数组,把每个绑定的 handler 句柄对象筛一次就好了。由于四个条件均是可选的,用了一个!xxx || 的判断,保证如果存在才判断相等,不存在则跳过。

remove() 函数具体实现

接下来就很简单了。

  function remove(element, events, fn, selector, capture){    var id = zid(element)    ;(events || '').split(/\s/).forEach(function(event){      // 关注点 1 (找出符合条件的 handler 数组)      findHandlers(element, event, fn, selector).forEach(function(handler){        // 关注点 2 (删除数组元素)        delete handlers[id][handler.i]      if ('removeEventListener' in element)        element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))      })    })  }

找到了对应的 handler 之后(可能不止一个哦),直接粗暴的 delete 掉.. 性能是保证了,但是空间浪费了,仍然会有一个undefined 的“坑”留在那里,也并不会被后续绑定的 handler 填上。不过鉴于一个元素也绑定不了多少事件回调函数,也就凑合用了。

较早之前我原以为会是维护队列或者链式调用的,没想到竟然..

至于 useCapture 为 true 或 false 的 listener 会被认为是两个不同的,因此移除事件监听也要把这个参数考虑进去。

事件的触发

首先来看 $.fn.triggerHandler() ,它只触发与事件相关的回调,并不会真正地把事件派发。

  // 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){      // 关注点 1 (包裹一层事件代理)      e = createProxy(isString(event) ? $.Event(event) : event)      e._args = args      e.target = element      // 关注点 2 (直接触发所有关联的句柄对象)      $.each(findHandlers(element, event.type || event), function(i, handler){        result = handler.proxy(e)        if (e.isImmediatePropagationStopped()) return false      })    })    return result  }

这里首先会做一个事件代理,避免直接修改原生的事件对象。在找到对应事件类型的 handler 后由其代理函数 handler.proxy() 执行响应。由于不经过 DOM 事件流,这种直接触发自然就不会冒泡。

在有多个 handler 的情况下如果被 stopImmediatePropagation() 了,则会终止遍历,不再触发后续 handler (我总感觉触发的顺序挺随机的)。该函数的返回值取决于最后一个 handler 响应事件的返回值。

另外还记得 handler.proxy() 中有一句:

        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))

只有手动触发事件才会有一个 _args 的参数,并直接传给回调函数。

再来看最后一个触发函数 $.fn.trigger()

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

这几乎已经没什么好分析的了,很简单的逻辑。可以纠结一下 this 指针,最外层的 this 指向调用 trigger() 的 Zepto 集合,而经过 .each() 之后指向的一般是单个 DOM 元素。如果事件的类型是 focus / blur 的话,可以直接调用 DOM 元素的原生方法,其他情况则通过 DOM 来派发事件。而如果this 不是一个 DOM 元素,则由 Zepto 包裹一次来直接触发与其相关联的句柄对象。

常用事件的快捷方法

对于一些常用事件,我们更希望采用如 el.click()el.error(()=>false) 的方法,而不是写一大串的el.on(...)el.bind(...) 等。

  // 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)    }  })

对于这些事件名,会在 Zepto 的原型上挂载相应的事件方法,如果不传参数,则达到触发事件的效果,传入一个回调函数,则为调用元素的该事件注册一个监听。

系列相关

一个普通的 Zepto 源码分析(一) - ie 与 form 模块
一个普通的 Zepto 源码分析(二) - ajax 模块
一个普通的 Zepto 源码分析(三) - event 模块




本文基于 知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 发布,欢迎引用、转载或演绎,但是必须保留本文的署名 BlackStorm 以及本文链接 http://www.cnblogs.com/BlackStorm/p/Zepto-Analysing-For-Event-Module.html ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系 。

原创粉丝点击