• 【zepto学习笔记03】事件机制


    前言

    我们今天直接进入事件相关的学习,因为近期可能会改到里面的代码
    就zepto来说,我认为最重要的就是选择器与事件相关了,随着浏览器升级,选择器简单了,而事件相关仍然是核心,今天我们就来学习学习

    zepto事件处理部分篇幅不大,不到400行,前面篇幅也很小,所以真的很适合移动开发

    变量定义

    1 var $$ = $.zepto.qsa,
    2 handlers = {}, _zid = 1,
    3 specialEvents = {},
    4 hover = {
    5     mouseenter: 'mouseover',
    6     mouseleave: 'mouseout'
    7 }

    事件部分首先定义了几个变量,$$为zepto选择器的方法,暂时不管他(据观察,好像也没有地方用到了,所以无意义)
    handlers为一个对象,与_zid息息相关,暂时不知道干什么的(据猜测两个应该是保存函数句柄,为removeEvent做准备)

    hover应该会同时触发两个事件才会触发,我们这里先不管,继续往下看

    这里提供一个zid飞方法,该方法用于保证-zid的唯一性

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

    算了,我们这里还是用一个实际点的方法跟进来吧,首先说一个关键的

    $.Event(type, [properties])

    这个方法比较关键,他可以创建一个dom事件,我们可以使用他来扩展新的事件对象,默认情况事件是可以冒泡的,可以设置
    这个事件可以通过trigger触发
    PS:trigger与Event是关键,各位一定要搞懂

     1 specialEvents = {}
     2 specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
     3 
     4 //根据参数创建一个event对象
     5 $.Event = function (type, props) {
     6     //当type是个对象时
     7     if (typeof type != 'string') props = type, type = props.type
     8     //创建一个event对象,如果是click,mouseover,mouseout时,创建的是MouseEvent,bubbles为是否冒泡
     9     var event = document.createEvent(specialEvents[type] || 'Events'),
    10     bubbles = true
    11     //确保bubbles的值为true或false,并将props参数的属性扩展到新创建的event对象上
    12     if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
    13     //初始化event对象,type为事件类型,如click,bubbles为是否冒泡,第三个参数表示是否可以用preventDefault方法来取消默认操作
    14     event.initEvent(type, bubbles, true, null, null, null, null, null, null, null, null, null, null, null, null)
    15     //添加isDefaultPrevented方法,event.defaultPrevented返回一个布尔值,表明当前事件的默认动作是否被取消,也就是是否执行了 event.preventDefault()方法.
    16     event.isDefaultPrevented = function () {
    17         return this.defaultPrevented
    18     }
    19     return event
    20 }

    这个方法的第二个参数我一般没看到出现,有时候我们可能会直接传入一个事件对象作为参数,所以我们这个时候第二个参数要保存第一个对象参数
    并将type获得,type还是必须是一个字符串

    然后此处创建了一个事件对象:

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

    createEvent

    Event对象的属性提供了有关事件的细节(比如事件发生到什么元素上),并且Event对象可以控制事件传播
    PS:IE这个家伙我已经不愿意去关注他了,标准事件模型中,Event对象会传递给句柄函数,但是IE会保存到window.event 中

    Event具有以下属性:

    event.bubbles

    事件冒泡类型,是冒泡则为true,非冒泡就是false

    事件传播分为三个阶段:
    ① 捕获阶段,事件由dom对象沿着文档树向下传播给目标节点,如果目标元素(或者parent)注册了事件,那么在事件传播过程中会执行
    ② 此阶段发生在目标节点自身,直接注册到目标上的适合事件会运行(比如本来要触发click,如果注册了mousedown也会运行)
    ③ 冒泡阶段,此阶段事件从目标元素向上传播到冒泡元素或者document

    所以我们在我们的回调函数中打印event.bubbles,如果是true的话,就说明是冒泡执行的事件

    cancelable

    该属性与我们的preventDefault有很大关联,事实上,preventDefault就是将该属性设置为false(也许吧......)

    event.currentTarget

    该属性返回监听事件的节点,即当前处理该事件的元素,在冒泡或捕获阶段,该元素比较有用
    比如我们将事件绑定到了div上,但是我们是点击里面的span而触发了div上面的事件,这个div就是currentTarget了

    eventPhase

    eventPhase 属性返回事件传播的当前阶段。它的值是下面的三个常量之一,它们分别表示捕获阶段、正常事件派发和起泡阶段

    event.targettarget

    事件属性可返回事件的目标节点(触发该事件的节点),如生成事件的元素、文档或窗口。

    以上面的例子为例,此时的target就是我们的span了

    event.timeStamp

    该属性,返回事件生成的日期

    event.type

    type 事件属性返回发生的事件的类型,即当前 Event 对象表示的事件的名称。
    它与注册的事件句柄同名,或者是事件句柄属性删除前缀 "on" 比如 "submit"、"load" 或 "click"。

    这里比较关键了,如果我们想注册自己的事件,比如tap,就可以这么干了,好了继续我们的话题

    event.stopPropagation()

    该方法将停止事件的传播,阻止它被分派到其他 Document 节点。在事件传播的任何阶段都可以调用它。注意,虽然该方法不能阻止同一个 Document 节点上的其他事件句柄被调用,但是它可以阻止把事件分派到其他节点。

    event.preventDefault()

    该方法将通知 Web 浏览器不要执行与事件关联的默认动作(如果存在这样的动作)。例如,如果 type 属性是 "submit",在事件传播的任意阶段可以调用任意的事件句柄,通过调用该方法,可以阻止提交表单。注意,如果 Event 对象的 cancelable 属性是 fasle,那么就没有默认动作,或者不能阻止默认动作。无论哪种情况,调用该方法都没有作用。
    比如,我们点击一个input时候在手机上会弹出键盘,我们如果使用e.preventDefault();就不会弹出

    event.initEvent(eventType,canBubble,cancelable)
    initEvent() 方法初始化新事件对象的属性。该方法必须在dispatchEvent调用前执行才有效

    PS:其实不止initEvent方法,还有initMouseEvent方法,这个方法还会提供鼠标坐标

    /*
    typeArg - 指定事件类型。
    canBubbleArg - 指定该事件是否可以 bubble。
    cancelableArg - 指定是否可以阻止事件的默认操作。
    viewArg - 指定 Event 的 AbstractView。
    detailArg - 指定 Event 的鼠标单击量。
    screenXArg - 指定 Event 的屏幕 x 坐标
    screenYArg - 指定 Event 的屏幕 y 坐标
    clientXArg - 指定 Event 的客户机 x 坐标
    clientYArg - 指定 Event 的客户机 y 坐标
    ctrlKeyArg - 指定是否在 Event 期间按下 control 键。
    altKeyArg - 指定是否在 Event 期间按下 alt 键。
    shiftKeyArg - 指定是否在 Event 期间按下 shift 键。
    metaKeyArg - 指定是否在 Event 期间按下 meta 键。
    buttonArg - 指定 Event 的鼠标按键。
    relatedTargetArg - 指定 Event 的相关 EventTarget。
    */
    clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);

    这个方法的应用我们后面点会涉及,这里暂时就不管他了,这里回到我们的Event方法

    他这里根据传入参数(type),创建了一个Event对象,此处的props如果是对象,那么就将赋给新建的Event对象
    然后使用initEvent初始化了我们Event参数的属性,并且添加了isDefaultPrevented方法,该方法可以知道当前事件的默认动作是否取消
    然后将这个对象返回
    既然都到了这里,我们来自定义一个鼠标事件吧

    自定义鼠标事件

    我们知道在移动端的click事件响应很慢,所以我们这里自己来实现一个fastclick的事件来替换本身的click

    http://sandbox.runjs.cn/show/scbpb0xg

    请使用手机测试点击相应速度

     1 <html xmlns="http://www.w3.org/1999/xhtml">
     2 <head>
     3     <title></title>
     4      <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
     5     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script>
     6     <script src="../../zepto.js" type="text/javascript"></script>
     7 </head>
     8 <body>
     9     <input type="button" value="我是普通点击事件" id="click" />
    10     <input type="button" value="我是快速点击事件" id="fastclick" />
    11 </body>
    12 <script type="text/javascript">
    13     var c = $('#click');
    14     var fc = $('#fastclick');
    15     var t = new Date().getTime()
    16     $(document).bind('touchstart', function (e) {
    17         t = e.timeStamp;
    18     });
    19     $(document).bind('touchend', function (e) {
    20         var event = $.Event('fastclick');
    21         //这里为了方便而已,其实该e.target
    22         fc[0].dispatchEvent(event);
    23     });
    24     c.bind('click', function (e) {
    25         $(this).val('我是普通点击事件' + '(' + (e.timeStamp - t) + ')')
    26     });
    27     fc.bind('fastclick', function (e) {
    28         $(this).val('我是快速点击事件' + '(' + (e.timeStamp - t) + ')')
    29     })
    30 </script>
    31 </html>

    我们这里为第二个按钮定义了一个fastclick事件,然后在touchend时候触发了该dom的事件
    大家点击时候自己就可以看到响应的速度(请使用手机测试)
    PS:这里各位一定要对dispatchEvent了解哦

    好了,我们继续回到我们的代码

    trigger/triggerHandler(event, [data])  

    这个trigger和dispatchEvent有莫大的关系,通过他我们可以触发一个事件,比如上面自定义事件可以通过他触发,我们看看他的源码

     1 $.fn.trigger = function (event, data) {
     2     if (typeof event == 'string' || $.isPlainObject(event)) event = $.Event(event)
     3     fix(event)
     4     event.data = data
     5     return this.each(function () {
     6         // items in the collection might not be DOM elements
     7         // (todo: possibly support events on plain old objects)
     8         if ('dispatchEvent' in this) this.dispatchEvent(event)
     9     })
    10 }

    他这里有个fix方法用于修复event对象,

     1 function fix(event) {
     2     if (!('defaultPrevented' in event)) {
     3         event.defaultPrevented = false //初始值false
     4         var prevent = event.preventDefault // 引用默认preventDefault
     5         event.preventDefault = function () { //重写preventDefault
     6             this.defaultPrevented = true
     7             prevent.call(this)
     8         }
     9     }
    10 }

    修复后,他就遍历我们包装的dom集合,然后依次触发该事件

    triggerHandler

    下面有一个triggerHandler事件与他相似,但是不会冒泡

     1 //触发元素上绑定的指定类型的事件,但是不冒泡
     2 $.fn.triggerHandler = function (event, data) {
     3     var e, result
     4     this.each(function (i, element) {
     5         e = createProxy(typeof event == 'string' ? $.Event(event) : event)
     6         e.data = data
     7         e.target = element
     8         //遍历元素上绑定的指定类型的事件处理函数集,按顺序执行,如果执行过stopImmediatePropagation,
     9         //那么e.isImmediatePropagationStopped()就会返回true,再外层函数返回false
    10         //注意each里的回调函数指定返回false时,会跳出循环,这样就达到的停止执行回面函数的目的
    11         $.each(findHandlers(element, event.type || event), function (i, handler) {
    12             result = handler.proxy(e)
    13             if (e.isImmediatePropagationStopped()) return false
    14         })
    15     })
    16     return result
    17 }

    这里有个findHandlers方法,我们来看看

     1 //查找绑定在元素上的指定类型的事件处理函数集合
     2 function findHandlers(element, event, fn, selector) {
     3     event = parse(event)
     4     if (event.ns) var matcher = matcherFor(event.ns)
     5     return (handlers[zid(element)] || []).filter(function (handler) {
     6         return handler && (!event.e || handler.e == event.e) //判断事件类型是否相同
     7     &&
     8     (!event.ns || matcher.test(handler.ns)) //判断事件命名空间是否相同
     9         //注意函数是引用类型的数据zid(handler.fn)的作用是返回handler.fn的标示符,如果没有,则给它添加一个,
    10         //这样如果fn和handler.fn引用的是同一个函数,那么fn上应该也可相同的标示符,
    11         //这里就是通过这一点来判断两个变量是否引用的同一个函数
    12     &&
    13     (!fn || zid(handler.fn) === zid(fn)) && (!selector || handler.sel == selector)
    14     })
    15 }

    我们前面说了,handlers应该保存的是我们的事件句柄集合,而我们可以东莞zid获取dom的唯一标识_zid
    PS:zepto将该属性保存至dom上,是因为dom属性不会变化可以帮助remove时候找到事件句柄

    其中使用了filter方法过来已有的集合,只取出与传入的事件(event)对象相同的事件(命名空间那些高级东西我们暂时不管吧)

    然后下面使用了proxy方法,我们来看看

     1 //设置代理
     2 $.proxy = function (fn, context) {
     3     if ($.isFunction(fn)) {
     4         //如果fn是函数,则申明一个新的函数并用context作为上下文调用fn
     5         var proxyFn = function () {
     6             return fn.apply(context, arguments)
     7         }
     8         //引用fn标示符
     9         proxyFn._zid = zid(fn)
    10         return proxyFn
    11     } else if (typeof context == 'string') {
    12         return $.proxy(fn[context], fn)
    13     } else {
    14         throw new TypeError("expected function")
    15     }
    16 }

    PS:我这里差点被坑了,我差点看成$.proxy方法了,其实不是的,这个handler.proxy是注册事件时候搞的一个东西,我们后面点说注册

    1 handler.proxy = function (e) {
    2     var result = callback.apply(element, [e].concat(e.data))
    3     //当事件处理函数返回false时,阻止默认操作和冒泡
    4     if (result === false) e.preventDefault(), e.stopPropagation()
    5     return result
    6 }

    嗯,由于这两的触发与注册事件相关,我们放到后面点再说,而且我发现triggerHandler没地方用的感觉......

    $.proxy(fn, context) 

    接受一个函数,然后返回一个新函数,并且这个新函数始终保持了特定的上下文语境,新函数中this指向context参数。另外一种形式,原始的function是context对像的方法。

     1 //设置代理
     2 $.proxy = function (fn, context) {
     3     if ($.isFunction(fn)) {
     4         //如果fn是函数,则申明一个新的函数并用context作为上下文调用fn
     5         var proxyFn = function () {
     6             return fn.apply(context, arguments)
     7         }
     8         //引用fn标示符
     9         proxyFn._zid = zid(fn)
    10         return proxyFn
    11     } else if (typeof context == 'string') {
    12         return $.proxy(fn[context], fn)
    13     } else {
    14         throw new TypeError("expected function")
    15     }
    16 }

    代理其实就是通过apply或者call改变javascript上下文作用域而已,我们不关注了,接下来就来到我们关键的地方了

    事件注册

    我们有很多方法可以为dom注册事件,而zepto为我们准备了统一的出口

    bind/one/delegate/live/on 都可以绑定事件,但是他们最终都是调用了add方法

    其中live私下是调用delegate,而且我们推荐使用on所以我们这里主要关注on吧,反正统一接口是add

    1 $.fn.on = function (event, selector, callback) {
    2     return !selector || $.isFunction(selector) ? this.bind(event, selector || callback) : this.delegate(selector, event, callback)
    3 }
    4 
    5 $.fn.bind = function (event, callback) {
    6     return this.each(function () {
    7         add(this, event, callback)
    8     })
    9 }
     1 //给元素绑定监听事件,可同时绑定多个事件类型,如['click','mouseover','mouseout'],也可以是'click mouseover mouseout'
     2 function add(element, events, fn, selector, getDelegate, capture) {
     3     var id = zid(element),
     4       set = (handlers[id] || (handlers[id] = [])) //元素上已经绑定的所有事件处理函数
     5     eachEvent(events, fn, function (event, fn) {
     6         var handler = parse(event)
     7         //保存fn,下面为了处理mouseenter, mouseleave时,对fn进行了修改
     8         handler.fn = fn
     9         handler.sel = selector
    10         // 模仿 mouseenter, mouseleave
    11         if (handler.e in hover) fn = function (e) {
    12             /* 
    13             relatedTarget为事件相关对象,只有在mouseover和mouseout事件时才有值
    14             mouseover时表示的是鼠标移出的那个对象,mouseout时表示的是鼠标移入的那个对象
    15             当related不存在,表示事件不是mouseover或者mouseout,mouseover时!$.contains(this, related)当相关对象不在事件对象内
    16             且related !== this相关对象不是事件对象时,表示鼠标已经从事件对象外部移入到了对象本身,这个时间是要执行处理函数的
    17             当鼠标从事件对象上移入到子节点的时候related就等于this了,且!$.contains(this, related)也不成立,这个时间是不需要执行处理函数的
    18             */
    19             var related = e.relatedTarget
    20             if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments)
    21         }
    22         //事件委托
    23         handler.del = getDelegate && getDelegate(fn, event)
    24         var callback = handler.del || fn
    25         handler.proxy = function (e) {
    26             var result = callback.apply(element, [e].concat(e.data))
    27             //当事件处理函数返回false时,阻止默认操作和冒泡
    28             if (result === false) e.preventDefault(), e.stopPropagation()
    29             return result
    30         }
    31         //设置处理函数的在函数集中的位置
    32         handler.i = set.length
    33         //将函数存入函数集中
    34         set.push(handler)
    35         element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    36     })
    37 }

    我们这里来详细看看我们的add方法

    ① 首先根据活动唯一的id(根据zid与dom而来)
    这里有个地方要注意,因为我们对统一dom可以注册多个事件,而他们的zid是相同的哦
    ② 取出元素上已经绑定的事件,第一次肯定是空数组
    ③ 第一个参数为dom,第二个为事件名称,第三个为回调函数,后面的我们暂时不关注
    ④ 使用eachEvent方法,该方法会遍历事件

    1 function eachEvent(events, fn, iterator) {
    2     if ($.type(events) != "string") $.each(events, iterator)
    3     else events.split(/s/).forEach(function (type) {
    4         iterator(type, fn)
    5     })
    6 }

    该方法具有三个参数,第一个参数为事件名(可以传入多个事件比如:"click mousemove")
    第二个参数是本身的回调函数,会在第三个参数(回调函数中作为参数传入)
    PS:总之他就是个遍历处理函数,你的typeName与回调函数会传进去
    ⑤ 使用parse解析事件类型

    1 //解析事件类型,返回一个包含事件名称和事件命名空间的对象
    2 function parse(event) {
    3     var parts = ('' + event).split('.')
    4     return {
    5         e: parts[0],
    6         ns: parts.slice(1).sort().join(' ')
    7     }
    8 }

    PS:为什么会有这个方法呢,因为我们使用backbone绑定事件时候是这个样子的click.delegateEvents......

    完了将回调函数与选择器(如果有的话)赋予handler,这里多的我们暂时不管
    ⑥ i表示当前函数在dom函数集中的位置,然后将他压入set,因为set为引用,所以原来handlers[id]的值也变了
    ⑦ 调用addEventListener绑定事件
    element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))

    至此事件绑定就结束了,我们顺便看看remove,因为所有的注销事件都是通过remove

    注销事件

     1 $.fn.off = function (event, selector, callback) {
     2     return !selector || $.isFunction(selector) ? this.unbind(event, selector || callback) : this.undelegate(selector, event, callback)
     3 }
     4 
     5 $.fn.unbind = function (event, callback) {
     6     return this.each(function () {
     7         remove(this, event, callback)
     8     })
     9 }
    10 
    11 function remove(element, events, fn, selector, capture) {
    12     var id = zid(element)
    13     eachEvent(events || '', fn, function (event, fn) {
    14         findHandlers(element, event, fn, selector).forEach(function (handler) {
    15             delete handlers[id][handler.i]
    16             element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    17         })
    18     })
    19 }

    我们直接对准remove开炮
    ① 首先根据dom获取唯一id
    ② 调用eachEvent方法依次处理
    ③ 根据findHandler找到当前type的事件类型集合
    ④ 删除数据句柄,然后移除dom事件

    这个比较简单,我们就不详说了

    简单写法

    我们经常这样绑定事件:
    el.click(function () {})
    那么这样是如何绑定事件的呢?答案在此

     1 ('focusin focusout load resize scroll unload click dblclick ' +
     2     'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
     3     'change select keydown keypress keyup error').split(' ').forEach(function (event) {
     4         $.fn[event] = function (callback) {
     5             return callback ?
     6             //如果有callback回调,则认为它是绑定
     7       this.bind(event, callback) :
     8             //如果没有callback回调,则让它主动触发
     9       this.trigger(event)
    10         }
    11     })

    以click来说相当于

    1 $.fn.click = function (fn) {
    2 this.bind('click', callback)
    3 }

    所以才绑定事件的啦......

    结语

    好了,今天暂时到这里,至此我们将zepto的核心就看的差不多了,剩下的我们再花一点时间说说就好了

  • 相关阅读:
    【转】ASP.NET MVC 使用 FluentScheduler 定时器计划任务
    【转】prototype扩展的JavaScript常用函数库
    【转】JavaScript系列文章:自动类型转换
    【转】Open Live Writer 插件更新
    highcharts与highstock实例
    SQL SERVER数据类型与C#数据类型对照表
    Highcharts在IE中不能一次性正常显示的一种解决办法
    MIME类型大全
    几种工具反编译被编译好的DLL文件
    MySQL之数据库的操作
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/3448500.html
Copyright © 2020-2023  润新知