你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
对角另一面

读Zepto源码之Event模块 0个回复 专栏 @ Javascript

对角另一面 发布于 3 月前

Event 模块是 Zepto 必备的模块之一,由于对 Event Api 不太熟,Event 对象也比较复杂,所以乍一看 Event 模块的源码,有点懵,细看下去,其实也不太复杂。

读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

准备知识

focus/blur 的事件模拟

为什么要对 focusblur 事件进行模拟呢?从 MDN 中可以看到, focus 事件和 blur 事件并不支持事件冒泡。不支持事件冒泡带来的直接后果是不能进行事件委托,所以需要对 focusblur 事件进行模拟。

除了 focus 事件和 blur 事件外,现代浏览器还支持 focusin 事件和 focusout 事件,他们和 focus 事件及 blur 事件的最主要区别是支持事件冒泡。因此可以用 focusin 和模拟 focus 事件的冒泡行为,用 focusout 事件来模拟 blur 事件的冒泡行为。

我们可以通过以下代码来确定这四个事件的执行顺序:

<input id="test" type="text" />
const target = document.getElementById('test')
target.addEventListener('focusin', () => {console.log('focusin')})
target.addEventListener('focus', () => {console.log('focus')})
target.addEventListener('blur', () => {console.log('blur')})
target.addEventListener('focusout', () => {console.log('focusout')})

chrome59下, input 聚焦和失焦时,控制台会打印出如下结果:

'focus'
'focusin'
'blur'
'focusout'

可以看到,在此浏览器中,事件的执行顺序应该是 focus > focusin > blur > focusout

关于这几个事件更详细的描述,可以查看:《说说focus /focusin /focusout /blur 事件

关于事件的执行顺序,我测试的结果与文章所说的有点不太一样。感兴趣的可以点击这个链接测试下http://jsbin.com/nizugazamo/edit?html,js,console,output。不过我觉得执行顺序可以不必细究,可以将 focusin 作为 focus 事件的冒泡版本。

mouseenter/mouseleave 的事件模拟

focusblur 一样,mouseentermouseleave 也不支持事件的冒泡, 但是 mouseovermouseout 支持事件冒泡,因此,这两个事件的冒泡处理也可以分别用 mouseovermouseout 来模拟。

在鼠标事件的 event 对象中,有一个 relatedTarget 的属性,从 MDN:MouseEvent.relatedTarget 文档中,可以看到,mouseoverrelatedTarget 指向的是移到目标节点上时所离开的节点( exited from ),mouseoutrelatedTarget 所指向的是离开所在的节点后所进入的节点( entered to )。

另外 mouseover 事件会随着鼠标的移动不断触发,但是 mouseenter 事件只会在进入节点的那一刻触发一次。如果鼠标已经在目标节点上,那 mouseover 事件触发时的 relatedTarget 为当前节点。

因此,要模拟 mouseentermouseleave 事件,只需要确定触发 mouseovermouseout 事件上的 relatedTarget 不存在,或者 relatedTarget 不为当前节点,并且不为当前节点的子节点,避免子节点事件冒泡的影响。

关于 mouseentermouseleave 的模拟, 谦龙 有篇文章《mouseenter与mouseover为何这般纠缠不清?》写得很清楚,建议读一下。

Event 模块的核心

Event 模块简化后如下:

;(function($){})(Zepto)

其实就是向闭包中传入 Zepto 对象,然后对 Zepto 对象做一些扩展。

Event 模块中,主要做了如下几件事:

  • 提供简洁的API
  • 统一不同浏览器的 event 对象
  • 事件句柄缓存池,方便手动触发事件和解绑事件。
  • 事件委托

内部方法

zid

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

获取参数 element 对象的 _zid 属性,如果属性不存在,则全局变量 _zid 增加 1 ,作为 element_zid 的属性值返回。这个方法用来标记已经绑定过事件的元素,方便查找。

parse

function parse(event) {
  var parts = ('' + event).split('.')
  return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
}

zepto 中,支持事件的命名空间,可以用 eventType.ns1.ns2... 的形式来给事件添加一个或多个命名空间。

parse 函数用来分解事件名和命名空间。

'' + event 是将 event 变成字符串,再以 . 分割成数组。

返回的对象中,e 为事件名, ns 为排序后,以空格相连的命名空间字符串,形如 ns1 ns2 ns3 ... 的形式。

matcherFor

function matcherFor(ns) {
  return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
}

生成匹配命名空间的表达式,例如,传进来的参数 nsns1 ns2 ns3 ,最终生成的正则为 /(?:^| )ns1.* ?ns2.* ?ns3(?: |$)/。至于有什么用,下面马上讲到。

findHandlers,查找缓存的句柄

handlers = {}
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.ns || matcher.test(handler.ns))
      && (!fn       || zid(handler.fn) === zid(fn))
      && (!selector || handler.sel == selector)
  })
}

查找元素对应的事件句柄。

event = parse(event)

调用 parse 函数,分隔出 event 参数的事件名和命名空间。

if (event.ns) var matcher = matcherFor(event.ns)

如果命名空间存在,则生成匹配该命名空间的正则表达式 matcher

return (handlers[zid(element)] || []).filter(function(handler) {
    ...
  })

返回的其实是 handlers[zid(element)] 中符合条件的句柄函数。 handlers 是缓存的句柄容器,用 element_zid 属性值作为 key

```javascript return handler // 条件1
&& (!event.e || handler.e == event.e) // 条件2 && (!event.ns || matcher.test(handler.ns)) // 条件3 && (!fn || zid(handler.fn) === zid(fn)) // 条件4 && (!selector || handler.sel == selector) // 条件5 ```

返回的句柄必须满足5个条件:

  1. 句柄必须存在
  2. 如果 event.e 存在,则句柄的事件名必须与 event 的事件名一致
  3. 如果命名空间存在,则句柄的命名空间必须要与事件的命名空间匹配( matcherFor 的作用 )
  4. 如果指定匹配的事件句柄为 fn ,则当前句柄 handler_zid 必须与指定的句柄 fn 相一致
  5. 如果指定选择器 selector ,则当前句柄中的选择器必须与指定的选择器一致

从上面的比较可以看到,缓存的句柄对象的形式如下:

{
  fn: '', // 函数
  e: '', // 事件名
  ns: '', // 命名空间
  sel: '',  // 选择器
  // 除此之外,其实还有
  i: '', // 函数索引
  del: '', // 委托函数
  proxy: '', // 代理函数
  // 后面这几个属性会讲到
}

realEvent,返回对应的冒泡事件

focusinSupported = 'onfocusin' in window,
focus = { focus: 'focusin', blur: 'focusout' },
hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
function realEvent(type) {
  return hover[type] || (focusinSupported && focus[type]) || type
}

这个函数其实是将 focus/blur 转换成 focusin/focusout ,将 mouseenter/mouseleave 转换成 mouseover/mouseout 事件。

由于 focusin/focusout 事件浏览器支持程度还不是很好,因此要对浏览器支持做一个检测,如果浏览器支持,则返回,否则,返回原事件名。

compatible,修正event对象

returnTrue = function(){return true},
returnFalse = function(){return false},
eventMethods = {
  preventDefault: 'isDefaultPrevented',
  stopImmediatePropagation: 'isImmediatePropagationStopped',
  stopPropagation: 'isPropagationStopped'
}

function compatible(event, source) {
  if (source || !event.isDefaultPrevented) {
    source || (source = event)

    $.each(eventMethods, function(name, predicate) {
      var sourceMethod = source[name]
      event[name] = function(){
        this[predicate] = returnTrue
        return sourceMethod && sourceMethod.apply(source, arguments)
      }
      event[predicate] = returnFalse
    })

    try {
      event.timeStamp || (event.timeStamp = Date.now())
    } catch (ignored) { }

    if (source.defaultPrevented !== undefined ? source.defaultPrevented :
        'returnValue' in source ? source.returnValue === false :
        source.getPreventDefault && source.getPreventDefault())
      event.isDefaultPrevented = returnTrue
      }
  return event
}

compatible 函数用来修正 event 对象的浏览器差异,向 event 对象中添加了 isDefaultPreventedisImmediatePropagationStoppedisPropagationStopped 几个方法,对不支持 timeStamp 的浏览器,向 event 对象中添加 timeStamp 属性。

if (source || !event.isDefaultPrevented) {
  source || (source = event)

  $.each(eventMethods, function(name, predicate) {
    var sourceMethod = source[name]
    event[name] = function(){
      this[predicate] = returnTrue
      return sourceMethod && sourceMethod.apply(source, arguments)
    }
    event[predicate] = returnFalse
  })

判断条件是,原事件对象存在,或者事件 eventisDefaultPrevented 不存在时成立。

如果 source 不存在,则将 event 赋值给 source, 作为原事件对象。

遍历 eventMethods ,获得原事件对象的对应方法名 sourceMethod

event[name] = function(){
  this[predicate] = returnTrue
  return sourceMethod && sourceMethod.apply(source, arguments)
}

改写 event 对象相应的方法,如果执行对应的方法时,先将事件中方法所对应的新方法赋值为 returnTrue 函数 ,例如执行 preventDefault 方法时, isDefaultPrevented 方法的返回值为 true

event[predicate] = returnFalse

这是将新添加的属性,初始化为 returnFalse 方法

try {
  event.timeStamp || (event.timeStamp = Date.now())
} catch (ignored) { }

这段向不支持 timeStamp 属性的浏览器中添加 timeStamp 属性。

if (source.defaultPrevented !== undefined ? source.defaultPrevented :
    'returnValue' in source ? source.returnValue === false :
    source.getPreventDefault && source.getPreventDefault())
  event.isDefaultPrevented = returnTrue
  }

这是对浏览器 preventDefault 不同实现的兼容。

source.defaultPrevented !== undefined ? source.defaultPrevented : '三元表达式'

如果浏览器支持 defaultPrevented, 则返回 defaultPrevented 的值

'returnValue' in source ? source.returnValue === false : '后一个判断'

returnValue 默认为 true,如果阻止了浏览器的默认行为, returnValue 会变为 false

source.getPreventDefault && source.getPreventDefault()

如果浏览器支持 getPreventDefault 方法,则调用 getPreventDefault() 方法获取是否阻止浏览器的默认行为。

判断为 true 的时候,将 isDefaultPrevented 设置为 returnTrue 方法。

createProxy,创建代理对象

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

zepto 中,事件触发的时候,返回给我们的 event 都不是原生的 event 对象,都是代理对象,这个就是代理对象的创建方法。

ignoreProperties 用来排除 A-Z 开头,即所有大写字母开头的属性,还有以returnValue 结尾,layerX/layerYwebkitMovementX/webkitMovementY 结尾的非标准属性。

for (key in event)
  if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

遍历原生事件对象,排除掉不需要的属性和值为 undefined 的属性,将属性和值复制到代理对象上。

最终返回的是修正后的代理对象

eventCapture

function eventCapture(handler, captureSetting) {
  return handler.del &&
    (!focusinSupported && (handler.e in focus)) ||
    !!captureSetting
}

返回 true 表示在捕获阶段执行事件句柄,否则在冒泡阶段执行。

如果存在事件代理,并且事件为 focus/blur 事件,在浏览器不支持 focusin/focusout 事件时,设置为 true , 在捕获阶段处理事件,间接达到冒泡的目的。

否则作用自定义的 captureSetting 设置事件执行的时机。

add,Event 模块的核心方法

function add(element, events, fn, data, selector, delegator, capture){
  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)
    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)
    if ('addEventListener' in element)
      element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
      })
}

add 方法是向元素添加事件及事件响应,参数比较多,先来看看各参数的含义:

element // 事件绑定的元素
events // 需要绑定的事件列表
fn // 事件执行时的句柄
data // 事件执行时,传递给事件对象的数据
selector // 事件绑定元素的选择器
delegator // 事件委托函数 
capture // 那个阶段执行事件句柄
var id = zid(element), set = (handlers[id] || (handlers[id] = []))

获取或设置 idset 为事件句柄容器。

events.split(/\s/).forEach(function(event){})

对每个事件进行处理

if (event == 'ready') return $(document).ready(fn)

如果为 ready 事件,则调用 ready 方法,中止后续的执行

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 上的一些属性,缓存起来。

这里主要看对 mouseentermouseleave 事件的模拟,具体的原理上面已经说过,只有在条件成立的时候才会执行事件句柄。

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
}

事件句柄的代理函数。

e 为事件执行时的原生 event 对象,因此先调用 compatiblee 进行修正。

调用 isImmediatePropagationStopped 方法,看是否已经执行过 stopImmediatePropagation 方法,如果已经执行,则中止后续程序的执行。

再扩展 e 对象,将 data 存到 edata 属性上。

执行事件句柄,将 e 对象作为句柄的第一个参数。

如果执行完毕后,显式返回 false,则阻止浏览器的默认行为和事件冒泡。

set.push(handler)
if ('addEventListener' in element)
  element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))

将句柄存入句柄容器

调用元素的 addEventListener 方法,添加事件,事件的回调函数用的是句柄的代理函数,eventCapture(handler, capture) 来用指定是否在捕获阶段执行。

remove,删除事件

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

首先获取指定元素的 _zid

;(events || '').split(/\s/).forEach(function(event){})

遍历需要删除的 events

findHandlers(element, event, fn, selector).forEach(function(handler){})

调用 findHandlers 方法,查找 event 下需要删除的事件句柄

delete handlers[id][handler.i]

删除句柄容器中对应的事件,在 add 函数中的句柄对象中的 i 属性就用在这里了,方便查找需要删除的句柄。

element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))

调用 removeEventListener 方法,删除对应的事件。

工具函数

$.event

$.event = { add: add, remove: remove }

add 方法和 remove 方法暴露出去,应该是方便第三方插件做扩展

$.proxy

$.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) }
    proxyFn._zid = zid(fn)
    return proxyFn
  } 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")
  }
}

代理函数,作用有点像 JS 中的 bind 方法,返回的是一个代理后改变执行上下文的函数。

var args = (2 in arguments) && slice.call(arguments, 2)

如果提供超过3个参数,则去除前两个参数,将后面的参数作为执行函数 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 的执行函数有两种传递方式,一是在第一个参数直接传入,二是第一个参数为上下文对象,执行函数也在上下文对象中一起传入。

这里判断 fn 是否为函数,即第一种传参方式,调用 fn 函数的 apply 方法,将上下文对象 context 作为 apply 的第一个参数,如果 args 存在,则与 fn 的参数合并。

给代理后的函数加上 _zid 属性,方便函数的查找。

else if (isString(context)) {
  if (args) {
    args.unshift(fn[context], fn)
    return $.proxy.apply(null, args)
  } else {
    return $.proxy(fn[context], fn)
  }

如果函数已经包含在上下文对象中,即第一个参数 fn 为对象,第二个参数 context 为字符串,用来指定执行函数的在上下文对象中的属性名。

if (args) {
  args.unshift(fn[context], fn)
  return $.proxy.apply(null, args)
}

如果参数存在时,将 fn[context] ,也即执行函数和 fn ,也即上下文对象放入 args 数组的开头,这样就将参数修正成跟第一种传参方式一样,再调用 $.proxy 函数。这里调用 apply 方法,是因为不知道参数有多少个,调用 apply 可以以数组的形式传入。

如果 args 不存在时,确定的参数项只有两个,因此可以直接调用 $.proxy 方法。

$.Event

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

specialEvents 是将鼠标事件修正为 MouseEvents ,这应该是处理浏览器的兼容问题,可能有些浏览器中,这些事件的事件类型并不是 MouseEvents

$.Event 方法用来手动创建特定类型的事件。

参数 type 可以为字符串,也可以为 event 对象。props 为扩展 event 对象的对象。

if (!isString(type)) props = type, type = props.type

如果不是字符串,也即是 event 对象时,将 type 赋给 propstype 为当前 event 对象中的 type 属性值。

var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true

调用 createEvent 方法,创建对应类型的 event 事件,并将事件冒泡默认设置为 true

if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])

遍历 props 属性,如果有指定 bubbles ,则采用指定的冒泡行为,其他属性复制到 event 对象上,实现对 event 对象的扩展。

event.initEvent(type, bubbles, true)
return compatible(event)

初始化新创建的事件,并将修正后的事件对象返回。

方法

.on()

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

on 方法来用给元素绑定事件,最终调用的是 add 方法,前面的一大段逻辑主要是修正参数。

var autoRemove, delegator, $this = this
if (event && !isString(event)) {
  $.each(event, function(type, fn){
    $this.on(type, selector, data, fn, one)
  })
  return $this
}

autoRemove 表示在执行完事件响应后,自动解绑的函数。

event 可以为字符串或者对象,当为对象时,对象的属性为事件类型,属性值为句柄。

这段是处理 event 为对象时的情况,遍历对象,得到事件类型和句柄,然后再次调用 on 方法,继续修正后续的参数。

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

先来分析第一个 ifselector 不为 stringcallback 不为函数,并且 callback 不为 false 时的情况。

这里可以确定 selector 并没有传递,因为 selector 不是必传的参数。

因此这里将 data 赋给 callbackselector 赋给 data ,将 selector 设置为 undefined ,因为 selector 没有传递,因此相应参数的位置都前移了一位。

再来看第二个 if ,如果 callback( 原来的 data ) 为 undefineddatafalse 时,表示 selector 没有传递,并且 data 也没有传递,因此将 data 赋给 callback ,将 data 设置为 undefined ,即将参数再前移一位。

第三个 if ,如果 callback === false ,用 returnFalse 函数代替,如果不用 returnFalse 代替,会报错。

return $this.each(function(_, element){
  add(element, event, callback, data, selector, delegator || autoRemove)
})

可以看到,这里是遍历元素集合,为每个元素都调用 add 方法,绑定事件。

if (one) autoRemove = function(e){
  remove(element, e.type, callback)
  return callback.apply(this, arguments)
}

如果只调用一次,设置 autoRemove 为一个函数,这个函数在句柄执行前,调用 remove 方法,将绑定在元素上对应事件解绑。

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

如果 selector 存在,表示需要做事件代理。

调用 closest 方法,从事件的目标元素 e.target 开始向上查找,返回第一个匹配 selector 的元素。关于 closest 方法,见《读Zepto源码之集合元素查找》分析。

如果 match 存在,并且 match 不为当前元素,则调用 createProxy 方法,为当前事件对象创建代理对象,再调用 $.extend 方法,为代理对象扩展 currentTargetliveFired 属性,将代理元素和触发事件的元素保存到事件对象中。

最后执行句柄函数,以代理元素 match 作为句柄的上下文,用代理后的 event 对象 evt 替换掉原句柄函数的第一个参数。

将该函数赋给 delegator ,作为代理函数传递给 add 方法。

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

解绑事件

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

这段逻辑与 on 方法中的相似,修正参数,不再细说。

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

第一个 if 是处理 selector 参数没有传递的情况的, selector 位置传递的其实是 callback

第二个 if 是判断如果 callbackfalse ,将 callback 赋值为 returnFalse 函数。

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

最后遍历所有元素,调用 remove 函数,为每个元素解绑事件。

.bind()

$.fn.bind = function(event, data, callback){
  return this.on(event, data, callback)
}

bind 方法内部调用的其实是 on 方法。

.unbind()

$.fn.unbind = function(event, callback){
  return this.off(event, callback)
}

unbind 方法内部调用的是 off 方法。

.one()

$.fn.one = function(event, selector, data, callback){
  return this.on(event, selector, data, callback, 1)
}

one 方法内部调用的也是 on 方法,只不过默认传递了 one 参数为 1 ,表示绑定的事件只执行一下。

.delegate()

$.fn.delegate = function(selector, event, callback){
  return this.on(event, selector, callback)
}

事件委托,也是调用 on 方法,只是 selector 一定要传递。

.undelegate()

$.fn.undelegate = function(selector, event, callback){
  return this.off(event, selector, callback)
}

取消事件委托,内部调用的是 off 方法,selector 必须要传递。

.live()

$.fn.live = function(event, callback){
  $(document.body).delegate(this.selector, event, callback)
  return this
}

动态创建的节点也可以响应事件。其实事件绑定在 body 上,然后委托到当前节点上。内部调用的是 delegate 方法。

.die()

$.fn.die = function(event, callback){
  $(document.body).undelegate(this.selector, event, callback)
  return this
}

将由 live 绑定在 body 上的事件销毁,内部调用的是 undelegate 方法。

.triggerHandler()

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

直接触发事件回调函数。

参数 event 可以为事件类型字符串,也可以为 event 对象。

```javascript e = createProxy(isString(event) ? $.Event(event) : event)
```

如果 event 为字符串时,则调用 $.Event 工具函数来初始化一个事件对象,再调用 createProxy 来创建一个 event 代理对象。

$.each(findHandlers(element, event.type || event), function(i, handler){
  result = handler.proxy(e)
  if (e.isImmediatePropagationStopped()) return false
    })

调用 findHandlers 方法来找出事件的所有句柄,调用 proxy 方法,即真正绑定到事件上的回调函数(参见 add 的解释),拿到方法返回的结果 result ,并查看 isImmediatePropagationStopped 返回的结果是否为 true ,如果是,立刻中止后续执行。

如果返回的结果 resultfalse ,也立刻中止后续执行。

由于 triggerHandler 直接触发回调函数,所以事件不会冒泡。

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

手动触发事件。

event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)

event 可以传递事件类型,对象和 event 对象。

如果传递的是字符串或者纯粹对象,则先调用 $.Event 方法来初始化事件,否则调用 compatible 方法来修正 event 对象,由于 $.Event 方法在内部其实已经调用过 compatible 方法修正 event 对象了的,所以外部不需要再调用一次。

if (event.type in focus && typeof this[event.type] == "function") this[event.type]()

如果是 focus/blur 方法,则直接调用 this.focus()this.blur() 方法,这两个方法是浏览器原生支持的。

如果 thisDOM 元素,即存在 dispatchEvent 方法,则用 dispatchEvent 来触发事件,关于 dispatchEvent ,可以参考 MDN: EventTarget.dispatchEvent()

否则,直接调用 triggerHandler 方法来触发事件的回调函数。

由于 trigger 是通过触发事件来执行事件句柄的,因此事件会冒泡。

系列文章

  1. 读Zepto源码之代码结构
  2. 读 Zepto 源码之内部方法
  3. 读Zepto源码之工具函数
  4. 读Zepto源码之神奇的$
  5. 读Zepto源码之集合操作
  6. 读Zepto源码之集合元素查找
  7. 读Zepto源码之操作DOM
  8. 读Zepto源码之样式操作
  9. 读Zepto源码之属性操作

参考

License

License: CC BY-NC-ND 4.0

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

等待第一条回复
登录后回复,如无账号,请使用邀请码注册