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

一个普通的 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)
      })
      // 返回 this 保证链式调用
      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 对象当作 event bus 的,每个 listener 只管订阅自己范围内的消息,事件的调度(派发、冒泡)是通过 DOM 事件流完成的(见 w3.org 的事件流图 ),这当然是实打实的发布/订阅模式。

基于 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 events

This means we can't just blindly extend all event properties onto the
event 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 元素额外维护一个映射表,直接给 DOM 元素添加 id 属性就好了,反过来我们还可以利用这个 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() 就是干这个的。

我们知道拥有多个命名空间的事件可能长这样: click.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 || yyy 的形式,保证只有存在才执行后面的相等判断,不存在则跳过。

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 ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系

posted @ 2017-08-14 13:09  BlackStorm  阅读(...)  评论(... 编辑 收藏