资讯专栏INFORMATION COLUMN

窥探zepto的事件模块

frolc / 1767人阅读

摘要:写在前面通过本文,您可以了解的事件模块,之后到底发生了什么样的事情,如何实现的事件委托,如何实现的自定义事件等问题。个人理解这么做目的在于便于移除事件,这样就可以使用匿名函数,而且仍可以将该匿名函数移除。

写在前面

通过本文,您可以了解zepto的事件模块,$(selector).on之后到底发生了什么样的事情,如何实现的事件委托【$(selector).delegate】,如何实现的自定义事件等问题。由于篇幅有限,移除事件部分,代码没有贴出,大家可以看这里(完整版)。先来看下全部API(略去了off等移除事件方法):

$.proxy

$.proxy(fn, context),用于改变函数的上下文,实现柯里化,有点类似于js原生的bind方法,支持两种传入参数方式:

fn为函数,context为对象,将fn内的this指向context

fn为对象,context为字符串,将调用fncontext方法

主要利用的apply来实现:

  // isString,isFunction为自定义的检测类型方法,实现很简单
  // (obj) => {}.toString.call(obj) === ["object String"]
  // isFunction也是同样方法即可
  $.proxy = function(fn, context) {
    var args = (2 in arguments) && slice.call(arguments, 2)
    if (isFunction(fn)) {
      // 利用apply调用,将上下文指定为context
      // 同时实现了柯里化
      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")
    }
  };
on

on被添加到了$.fn上,也就是$.prototype上,这样一来,每一个zepto都会有该方法,on方法十分关键,one,live,delegate,bind等方法可以视为on方法的语法糖。在来看on方法,首先来看一下add和几个工具方法:

function add(element, events, fn, data, selector, delegator, capture){
    // zid方法用于给element对象设置唯一id值或者取出element对应的id
    var id = zid(element),
        set = handlers[id] || (handlers[id] = []);
    // 多个事件传入时,利用空格来进行分离
    events.split(/s/).forEach(function(event) {
        if (event === "ready") 
            return $(document).ready(fn);
        // zepto支持事件的命名空间,parse用来解析出命名空间和事件名
        var handler = parse(event);
        handler.fn = fn;
        handler.sel = selector;
        if (handler.e in hover) {
            fn = function(e) {
                var related = e.relatedTarget;
                // relatedTarget属性仅对mouseout,mouseover有效
                // mouseover而言,是指移入目标节点前离开的那个节点
                // mouseout而言,是指移出目标节点进入的那个节点
                // e.relatedTarget等于目标节点且该节点不是目标节点的子节点
                // 则证明该事件已经触发完成,可以执行相应的事件处理函数
                // $.contain是查询this中是否包含related
                if (
                    !related || 
                    (this !== related &&
                    !$.contains(this, related))
                ) {
                    return handler.fn.apply(this, arguments);
                }
            };
        }
        handler.del = delegator;
        var callback = delegator || fn;
        handler.proxy = function(e) {
            // 给e添加属性
            e = compatible(e);
            if (e.isImmediatePropagationStopped()) {
                return void 0;
            }
            // 将data挂载到event对象上
            e.data = data;
            // return false => 进行阻止默认事件与事件冒泡
            // callback函数绑定在了element上
            // _args仅仅在trigger和triggerHandler方式调用时才有
            var result = callback.apply(element, e._args == null ? [e] : [e].concat(e._args));
            if (result === false) {
                e.preventDefault();
                e.stopPropagation();
            }
            return result;
        };
        // 标记handler位置,方便删除
        handler.i = set.length;
        set.push(handler);
        // 添加事件
        // realEvent返回可行的事件名,因为zepto中会尝试
        // 将mouseenter转为mouseover,mouseleave转为mouseout
        // 将focus转为focusin,blur转为focusout
        if ("addEventListener" in element) {
            element.addEventListener(
                realEvent(event), 
                handler.proxy, 
                eventCapture(handler, capture)
            );
        }
    });
}

需要注意几点:

zepto会利用compatible给事件对象e添加三个方法:

isDefaultPrevented是否执行了preventDefault来阻止默认事件

isPropagationStopped是否执行了stopPropagation来阻止冒泡

isImmediatePropagationStopped是否执行了stopImmediatePropagation,阻止冒泡的同时阻止该元素上其余事件的执行

e对象有两种情况:

原生的事件对象

进行事件委托时,zepto给原生的设置一个代理对象,该对象由createProxy方法生成

handlers可以理解为一个缓存池,通过zid会对每一个元素生成一个唯一id值,每一个handlers[id]对应一个数组对象,该数组中存储着的是每一个在这个元素上绑定的事件及其执行函数等信息。(个人理解这么做目的在于便于移除事件,这样就可以使用匿名函数,而且仍可以将该匿名函数移除)。

addEventListener的第三个参数决定了事件执行的阶段

默认为false,也就是在冒泡阶段来执行,

true表示在捕获阶段执行

eventCapture方法来确定事件在哪个阶段执行,对于事件委托的情况来说,在捕获节点时执行,而对于focusblur由于不支持事件冒泡,所以只好利用事件捕获来模拟事件冒泡

理解了add后,再看来on方法:

// 绑定事件
// event可以是对象,也可是字符串
// 只传入event,data,callback时,为bind方法
// 只传入event,selector,callback时为delegate or live
// 传入了one参数时,则为one方法
L.fn.on = function(event, selector, data, callback, one) {
    var autoRemove, 
        delegator, 
        $this = this;
    // 假若传入的event为对象,key为事件名,fn为执行函数
    if (event && !L.isString(event)) {
        $.each(event, function(type, fn) {
            $this.on(type, selector, data, fn, one);
        });
        return $this; 
    }
    // 假如未传入selector的情况,bind就是这种情况
    if (!isString(selector) && !isFunction(callback) && callback !== false) {
        callback = data;
        data = selector;
        selector = undefined;            
    }    
    // data未传入,将callback设为data对应的值
    if (callback === undefined || data === false) {
        callback = data;
        data = undefined;
    }
    // 传入false为处理函数,默认为阻止默认事件,阻止冒泡
    if (callback === false) callback = returnFalse;
    return $this.each(function(_, element) {
        if (one) {
            autoRemove = function(e) {
                // one为绑定的事件函数执行一次后,就移除该事件
                remove(element, e.type, callback);
                return callback.apply(this, arguments);
            };
        }
        if (selector) {
            // 事件委托实现
            delegator = function(e) {
                var evt,
                    match = L(e.target).closest(selector, element).get(0);
                if (match && match !== element) {
                    // 生成代理事件对象,事件委托中传入执行函数的event对象并不是原生事件对象
                    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);
    });
};

bind方法是on的一个语法糖,其只接受了三个参数,event, data, callback,同理delegate,live,one等也是一样,只是利用不同的传参数方式来调用on方法。

自定义事件

利用$.Event可以创建并且初始化一个DOM事件:

// 创建自定义事件
// bubbles,默认为true,冒泡时处理
L.Event = function(type, props) {
    if (!L.isString(type)) {
        props = type;
        type = props.type;
    }
    var event = document.createEvent(specialEvents[type] || "Events"),
        bubbles = true;
    if (props) {
        for (var name in props) {
            if (name === "bubbles") {
                bubbles = !!props[name];
            } else {
                event[name] = bubbles[name];
            }
        }
    }
    // initEvent初始化事件对象
    // 事件类型,是否冒泡,是否可以阻止默认事件
    event.initEvent(type, bubbles, true);
    return compatible(event);
};

利用trigger可以触发事件,trigger还支持传入参数:

// 触发事件,zepto的触发事件只能作用于DOM上
L.fn.trigger = function(event, args) {
    event = (L.isString(event) || L.isPlainObject(event)) ? L.Event(event) : compatible(event);
    event._args = args;
    return this.each(function() {
        if (event.type in focus && typeof this[event.type] === "function") {
            this[event.type]();
        } else if ("dispatchEvent" in this) {  // 浏览器原生触发事件API
            // 默认采用浏览器原生方法来触发自定义事件
            this.dispatchEvent(event);
        } else {
            // 假若不支持,则调用triggerHandler
            // 此方法不会冒泡,只会在当前元素上触发
            // 实现是:根据对应事件筛选出其执行函数,调用其执行函数
            $(this).triggerHandler(event, args);
        }
    });
};
简化

使用zepto时,对于click等事件来说并不用进行bind而是直接调用$().click()就好:

("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) {
    // 传参数为注册,不传为触发
    L.fn[event] = function(callback) {
      return (arguments.length === 1) ?
        this.bind(event, callback) :
        this.trigger(event)
    }
});

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86547.html

相关文章

  • Zepto源码之Gesture模块

    摘要:模块基于上的事件的封装,利用属性,封装出系列事件。这个判断需要引入设备侦测模块。然后是监测事件,根据这三个事件,可以组合出和事件。其中变量对象和模块中的对象的作用差不多,可以先看看读源码之模块对模块的分析。 Gesture 模块基于 IOS 上的 Gesture 事件的封装,利用 scale 属性,封装出 pinch 系列事件。 读 Zepto 源码系列文章已经放到了github上,欢...

    coolpail 评论0 收藏0
  • Zepto源码之Touch模块

    摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。 大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swip...

    Prasanta 评论0 收藏0
  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    spacewander 评论0 收藏0
  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    fuyi501 评论0 收藏0
  • zepto.js学习如何手动(trigger)触发DOM事件

    摘要:好啦我们已经解决了是啥的疑问了,现在回去开始一步步解读如何实现手动触发事件。我们主要看看这里面几乎含有如何手动触发一个事件的大部分步骤和内容。 前言 前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,咱们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后往往也是无尽的浮躁,学习这些先进流行的类库或者框架可以让我们走的更快,但是静下心...

    Julylovin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<