jQuery源码解析之click的事件绑定

41次阅读

共计 7199 个字符,预计需要花费 18 分钟才能阅读完成。

前言:
这篇依旧长,请耐心看下去。

一、事件委托
DOM 有个 事件流 特性,所以触发 DOM 节点的时候,会经历 3 个阶段:
(1)阶段一:Capturing 事件捕获(从祖到目标)
事件 自上(document->html->body->xxx)而下到达目标节点的过程中,浏览器会检测 针对该事件的 监听器(用来捕获事件),并运行 捕获事件的监听器

(2)阶段二:Target 目标
浏览器找到监听器后,就运行该监听器

(3)阶段三:Bubbling 冒泡(目标到祖)
事件 自下而上(document->html->body->xxx)到达目标节点的过程中,浏览器会检测 不是 针对该事件的 监听器(用来捕获事件),并运行 非捕获事件的监听器

二、$().click()
作用:
为目标元素绑定点击事件

源码:

  // 这种写法还第一次见,将所有鼠标事件写成字符串再换成数组
  // 再一一绑定到 DOM 节点上去
  // 源码 10969 行
  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 ?
          // 如果有参数的话,就用 jQuery 的 on 绑定
          this.on(name, null, data, fn) :
          // 否则使用 trigger
          this.trigger(name);
      };
    } );

解析:
可以看到,jQuery 将所有的鼠标事件都一一列举了出来,并通过jQuery.fn[name] = function(data, fn) {xxx}

如果有参数,则是绑定事件,调用 on() 方法;
没有参数,则是调用事件,调用 trigger() 方法(trigger() 放到下篇讲)

三、$().on()
作用:
在被选元素及子元素上添加一个或多个事件处理程序

源码:

  // 绑定事件的方法
  // 源码 5812 行
  jQuery.fn.extend( {
    // 在被选元素及子元素上添加一个或多个事件处理程序
    //$().on('click',function()=<{})
    // 源码 5817 行
    on: function(types, selector, data, fn) {return on( this, types, selector, data, fn);
    },
    //xxx
    //xxx
})

最终调用的是 jQuery.on() 方法:

  // 绑定事件的 on 方法
  // 源码 5143 行
  // 目标元素,类型(click,mouseenter,focusin,xxx), 回调函数 function(){xxx}
  function on(elem, types, selector, data, fn, one) {
    var origFn, type;
    // 这边可以不看
    // Types can be a map of types/handlers
    if (typeof types === "object") {// ( types-Object, selector, data)
      if (typeof selector !== "string") {// ( types-Object, data)
        data = data || selector;
        selector = undefined;
      }
      for (type in types) {on( elem, type, selector, data, types[ type], one );
      }
      return elem;
    }
    // 直接调用 $().on()的话会走这边
    if (data == null && fn == null) {// ( types, fn)
      //fn 赋值为 selector,即 function(){}
      fn = selector;
      // 再将 selector 置为 undefined
      // 注意这个写法,连等赋值
      data = selector = undefined;
    }
    // 调用像 $().click()的话会走这边
    else if (fn == null) {if ( typeof selector === "string") {// ( types, selector, fn)
        fn = data;
        data = undefined;
      } else {// ( types, data, fn)
        fn = data;
        data = selector;
        selector = undefined;
      }
    }

    if (fn === false) {fn = returnFalse;} else if (!fn) {return elem;}
    //one()走这里
    if (one === 1) {
      // 将 fn 赋给 origFn 后,再定义 fn
      origFn = fn;
      fn = function(event) {
        // 将绑定给目标元素的事件传给 fn,// 并通过 $().off()卸载掉
        // Can use an empty set, since event contains the info
        jQuery().off( event);
        // 在 origFn 运行一次的基础上,让 origFn 调用 fn 方法,arguments 即 event
        return origFn.apply(this, arguments);
      };
      // 让 fn 和 origFn 使用相同的 guid,这样就能移除 origFn 方法
      // Use same guid so caller can remove using origFn
      fn.guid = origFn.guid || (origFn.guid = jQuery.guid++);
    }

    return elem.each(function() {
      // 最终调动 $.event.add 方法
      jQuery.event.add(this, types, fn, data, selector);

    } );
  }

解析:
可以看到,由于将 bind()、live() 和 delegate() 都合并进 on() 后,on() 里面的情况挺复杂的,data、selector、fn 相互赋值。

注意下 if (one === 1) 这种情况,是 $().one()on() 里的具体实现,即调用一次 on() 后,就执行jQuery().off( event),卸载事件。

该方法最终调用 jQuery.event.add() 方法

四、jQuery.event.add()
作用:
为目标元素添加事件

源码:

 // 源码 5235 行
  /*
 * Helper functions for managing events -- not part of the public interface.
 * Props to Dean Edwards' addEvent library for many of the ideas.
 */
  jQuery.event = {global: {},
    // 源码 5241 行
    //this, types, fn, data, selector
    add: function(elem, types, handler, data, selector) {

      var handleObjIn, eventHandle, tmp,
        events, t, handleObj,
        special, handlers, type, namespaces, origType,
        //elemData 正是目标元素 jQuery 中的 id 属性
        // 初始值是{}
        elemData = dataPriv.get(elem);
      // Don't attach events to noData or text/comment nodes (but allow plain objects)
      if (!elemData) {return;}
      // 调用者可以传入一个自定义数据对象来代替处理程序
      // 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 有唯一的 id
      // Make sure that the handler has a unique ID, used to find/remove it later
      if (!handler.guid) {handler.guid = jQuery.guid++;}
      // 如果事件处理没有,则置为空对象
      // Init the element's event structure and main handler, if this is the first
      // 在这里,就应经给 events 赋值了,// 注意这种写法: 赋值的同时,判断
      if (!( events = elemData.events) ) {events = elemData.events = {};
      }

      if (!( eventHandle = elemData.handle) ) {eventHandle = elemData.handle = function( e) {// 当在一个页面卸载后调用事件时,取消 jQuery.event.trigger()的第二个事件
          // Discard the second event of a jQuery.event.trigger() and
          // when an event is called after a page has unloaded

          //jQuery.event.triggered: undefined
          //e.type: click/mouseout
          return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
            // 让 elem 调用 jQuery.event.dispatch 方法,参数是 arguments
            jQuery.event.dispatch.apply(elem, arguments) : undefined;
        };
      }
      // 通过空格将多个 events 分开,一般为一个,如 click
      // Handle multiple events separated by a space
      types = (types || "").match(rnothtmlwhite) || [""];
      t = types.length;
      while (t--) {tmp = rtypenamespace.exec( types[ t] ) || [];
        //click
        type = origType = tmp[1];
        //""namespaces = (tmp[ 2] ||"" ).split(".").sort();
        // There *must* be a type, no attaching namespace-only handlers
        if (!type) {continue;}
        // 如果 event 改变了它自己的 type,就使用特殊的 event handlers
        // If event changes its type, use the special event handlers for the changed type
        special = jQuery.event.special[type] || {};
        // 如果选择器已定义,确定一个特殊 event api 的 type
        // 否则使用默认 type
        // If selector defined, determine special event api type, otherwise given type
        type = (selector ? special.delegateType : special.bindType) || type;
        // 不明白为什么在上面要先写一遍
        // Update special based on newly reset type
        special = jQuery.event.special[type] || {};
        //handleObj 会传递给所有的 event handlers
        // 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 );
        
        // 第一次绑定事件,走这里
        // Init the event handler queue if we're the first
        if (!( handlers = events[ type] ) ) {handlers = events[ type] = [];
          handlers.delegateCount = 0;

          // Only use addEventListener if the special events handler returns false
          if ( !special.setup ||
            special.setup.call(elem, data, namespaces, eventHandle) === false ) {
            // 目标元素有 addEventListener 的话,调用绑定 click 事件
            if (elem.addEventListener) {elem.addEventListener( type, eventHandle);
            }
          }
        }
        //special 的 add/handleObj.handler.guidd 的初始化处理
        if (special.add) {special.add.call( elem, handleObj);

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

        // 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;
      }

    },

...
...

}

解析:
可以看到,很多的 if 判断,都是在初始化对象,最后通过 while 循环,调用目标元素的 addEventListener 事件,也就是说,click()/on() 的本质是 element.addEventListener() 事件,前面一系列的铺垫,都是在为目标 jQuery 对象添加必要的属性。

注意写法 if (!( events = elemData.events) ),在赋值的同时,判断条件

(1)dataPriv

  // 取唯一 id
  // 源码 4361 行
  var dataPriv = new Data();

在 jQuery 对象中,有唯一 id 的属性

$("#one")

elemData = dataPriv.get(elem)

① Data()

  // 目标元素的 jQuery id
  // 源码 4209 行
  function Data() {this.expando = jQuery.expando + Data.uid++;}

② jQuery.expando

  jQuery.extend( {
    // 相当于 jQuery 为每一个元素取唯一的 id
    ///\D/g : 去掉非数字的字符
    // Unique for each copy of jQuery on the page
    // 源码 360 行
    expando: "jQuery" + (version + Math.random() ).replace(/\D/g, ""),
    ...  
    ...
})

③ Math.random()
伪随机,到小数点后 16 位

expando: "jQuery" + (version + Math.random() ).replace(/\D/g, ""),

可以看到 jQuery 的 id 是由 jQuery + 版本号 + Math.random() 生成的

关于 Math.random() 是如何生成伪随机数的请看:https://www.zhihu.com/question/22818104

(2)rtypenamespace

  var
    rkeyEvent = /^key/,
    rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
    // 事件类型的命名空间
    // 举例:var arr1 = "click.aaa.bbb".match(rtypenamespace);
    //console.log(arr1);//["click.aaa.bbb", "click", "aaa.bbb", index: 0, input: "click.aaa.bbb"]
    // 源码 5131 行
    rtypenamespace = /^([^.]*)(?:\.(.+)|)/;

综上,绑定事件的本质即调用 element.addEventListener() 方法,但 jQuery 有太多的情况需要考虑了。


(完)

正文完
 0