窥探zepto的事件模块

zp1996

写在前面

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

clipboard.png

$.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)
    }
});
阅读 2.4k

zp的笔记
一些个人笔记

coder

3.2k 声望
49 粉丝
0 条评论
你知道吗?

coder

3.2k 声望
49 粉丝
宣传栏