jQuery 事件绑定

Posted by franki on June 9, 2019

事件堪比javascript的心脏,页面的更新变化,几乎都是通过事件的响应去触发的,由此可见,事件在javascript的地位。

开始正文之前,很有必要补充下事件的两个概念,一个是捕获、一个是冒泡,如下图所示

event-execute

这个图很好的解释了事件的执行过程,对于浏览器常见的如click、mouse事件就可以使用上图的机制,去解决现实场景的问题。

回归正文,jQuery对事件的绑定主要总结如下:

.bind()

.live()

.delegate()

.on()

以上的这些方法,最终都是调用jQuery.fn.on方法。

而jQuery自身有提供click方法,首先看看在jQuery中,click方法是如何实现的?

jQuery.each(
    (
        'blur focus focusin focusout resize scroll click dblclick ' +
        'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
        'change select submit keydown keypress keyup contextmenu'
    ).split(' '),
    function(i, name) {
        // Handle event binding
        jQuery.fn[name] = function(data, fn) {
            return arguments.length > 0
                ? this.on(name, null, data, fn)
                : this.trigger(name);
        };
    }
);

看到了吗,就是把这样的一个个字符串的值,添加入jQuery.fn[name]中,形成了jQuery对象中的方法。

下面看一下,平常使用jQuery实现元素的点击事件

var test = $('#test');
test.click(function() {
    console.log(111);
});

上面这段代码在jQuery里面发生了什么呢?

注: jQuery的事件机制是借鉴Dean Edwards的,从源码的这句注释可知:

/*
  * Helper functions for managing events -- not part of the public interface.
  * Props to Dean Edwards' addEvent library for many of the ideas.
  */

继续用图说明,jQuery事件处理流程

event-on

先来看add的实现

/*
 * elem 要操作的元素
 * types 事件类型
 * handler 事件触发的方法
 * data 事件触发的方法需要的参数
 * selector 委托的元素
 */
add: function(elem, types, handler, data, selector) {
    var handleObjIn,
        eventHandle,
        tmp,
        events,
        t,
        handleObj,
        special,
        handlers,
        type,
        namespaces,
        origType,
        // 获取绑定在elem上的data缓存
        elemData = dataPriv.get(elem);

    // Don't attach events to noData or text/comment nodes (but allow plain objects)
    // 不容许出现空数据或者是文本/注释节点,容许空对象
    if (!elemData) {
        return;
    }

    // handler容许对象代替handler函数,这里的对象指的是下面提到的handleObj
    // Caller can pass in an object of custom data in lieu of the handler
    if (handler.handler) {
        handleObjIn = handler;
        handler = handleObjIn.handler;
        selector = handleObjIn.selector;
    }

    // 确认是有效的选择器
    // Ensure that invalid selectors throw exceptions at attach time
    // Evaluate against documentElement in case elem is a non-element node (e.g., document)
    if (selector) {
        jQuery.find.matchesSelector(documentElement, selector);
    }

    // 给handler添加一个guid属性,确保handler函数有唯一id,之后的查找或者删除可以使用它
    // Make sure that the handler has a unique ID, used to find/remove it later
    if (!handler.guid) {
        handler.guid = jQuery.guid++;
    }

    // 第一次元素节点没有handler的数据属性,这边需要给元素节点初始化,给其加上events属性
    // Init the element's event structure and main handler, if this is the first
    if (!(events = elemData.events)) {
        events = elemData.events = {};
    }
    // 给elemData加上handle属性
    if (!(eventHandle = elemData.handle)) {
        eventHandle = elemData.handle = function(e) {
            // Discard the second event of a jQuery.event.trigger() and
            // when an event is called after a page has unloaded
            return typeof jQuery !== 'undefined' &&
                jQuery.event.triggered !== e.type
                ? jQuery.event.dispatch.apply(elem, arguments)
                : undefined;
        };
    }

    // 处理用空格分开的多事件操作
    // Handle multiple events separated by a space
    types = (types || '').match(rnothtmlwhite) || [''];
    t = types.length;
    while (t--) {
        tmp = rtypenamespace.exec(types[t]) || [];
        type = origType = tmp[1];
        namespaces = (tmp[2] || '').split('.').sort();

        // There *must* be a type, no attaching namespace-only handlers
        if (!type) {
            continue;
        }

        // 得到特殊处理的事件类型
        // If event changes its type, use the special event handlers for the changed type
        special = jQuery.event.special[type] || {};

        // 如果selector有定义,type可能不一样
        // If selector defined, determine special event api type, otherwise given type
        type =
            (selector ? special.delegateType : special.bindType) ||
            type;

        // 修正special
        // Update special based on newly reset type
        special = jQuery.event.special[type] || {};

        // 得到handleObj
        // handleObj is passed to all event handlers
        handleObj = jQuery.extend(
            {
                type: type,
                origType: origType,
                data: data,
                handler: handler,
                guid: handler.guid,
                selector: selector,
                needsContext:
                    selector &&
                    jQuery.expr.match.needsContext.test(selector),
                namespace: namespaces.join('.')
            },
            handleObjIn
        );

        // 初始化elemData.events[type]
        // Init the event handler queue if we're the first
        if (!(handlers = events[type])) {
            handlers = events[type] = [];
            handlers.delegateCount = 0;

            // 在没有special.setup 或者 special.setup返回false的时候,对元素绑定事件
            // Only use addEventListener if the special events handler returns false
            if (
                !special.setup ||
                special.setup.call(
                    elem,
                    data,
                    namespaces,
                    eventHandle
                ) === false
            ) {
                if (elem.addEventListener) {
                    elem.addEventListener(type, eventHandle);
                }
            }
        }

        if (special.add) {
            special.add.call(elem, handleObj);

            if (!handleObj.handler.guid) {
                handleObj.handler.guid = handler.guid;
            }
        }

        // 把handleObj附加到events[type]数组。对于委托事件,delegateCount++并且添加到数组最前面。否则添加到数组最后面
        // Add to the element's handler list, delegates in front
        if (selector) {
            handlers.splice(handlers.delegateCount++, 0, handleObj);
        } else {
            handlers.push(handleObj);
        }

        // Keep track of which events have ever been used, for event optimization
        jQuery.event.global[type] = true;
    }
}

这个add方法,总的来说,就是对于节点上面的data缓存数据进行添加或者修正。之后大部分的操作都是集中在这个数据缓存elemDATA上。主要的目的就是让元素绑定了事件。就是这句代码

elem.addEventListener(type, eventHandle);

下面是jQuery.events.dispatch方法讲解

dispatch: function(nativeEvent) {
    // 修正事件源对象
    // Make a writable jQuery.Event from the native event object
    var event = jQuery.event.fix(nativeEvent);

    var i,
        j,
        ret,
        matched,
        handleObj,
        handlerQueue,
        args = new Array(arguments.length),
        // 得到节点所绑定的事件对象
        handlers =
            (dataPriv.get(this, 'events') || {})[event.type] || [],
        special = jQuery.event.special[event.type] || {};

    // Use the fix-ed jQuery.Event rather than the (read-only) native event
    args[0] = event;

    for (i = 1; i < arguments.length; i++) {
        args[i] = arguments[i];
    }

    event.delegateTarget = this;

    // Call the preDispatch hook for the mapped type, and let it bail if desired
    if (
        special.preDispatch &&
        special.preDispatch.call(this, event) === false
    ) {
        return;
    }

    // 得到handler的执行队列
    // Determine handlers
    handlerQueue = jQuery.event.handlers.call(this, event, handlers);

    // Run delegates first; they may want to stop propagation beneath us
    i = 0;
    while (
        (matched = handlerQueue[i++]) &&
        !event.isPropagationStopped()
    ) {
        event.currentTarget = matched.elem;

        j = 0;
        while (
            (handleObj = matched.handlers[j++]) &&
            !event.isImmediatePropagationStopped()
        ) {
            // 不存在命名空间,或者匹配的命名空间
            // If the event is namespaced, then each handler is only invoked if it is
            // specially universal or its namespaces are a superset of the event's.
            if (
                !event.rnamespace ||
                handleObj.namespace === false ||
                event.rnamespace.test(handleObj.namespace)
            ) {
                event.handleObj = handleObj;
                event.data = handleObj.data;
                // 执行绑定函数
                ret = (
                    (jQuery.event.special[handleObj.origType] || {})
                        .handle || handleObj.handler
                ).apply(matched.elem, args);
                // 如果是return false,阻止默认事件,阻止冒泡
                if (ret !== undefined) {
                    if ((event.result = ret) === false) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
    }

    // Call the postDispatch hook for the mapped type
    if (special.postDispatch) {
        special.postDispatch.call(this, event);
    }

    return event.result;
}

dispatch方法,主要是用来分配触发回调函数,通过jQuery.fn.dispatch.apply(…)从而到执行绑定函数。

这部分是对于绑定到执行了整个流程,后面会对具体的委派事件和自定义事件进行讲解,未完待续!