jQuery源码解析之trigger

45次阅读

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

一、$().trigger()和$().triggerHandler() 的作用和区别

(1)trigger(“focus”) 触发被选元素上的指定事件(focus)以及事件的默认行为(比如表单提交);
triggerHandler(xxx) 不会引起事件(比如表单提交)的默认行为

(2)trigger(xxx) 触发所有匹配元素的指定事件;
triggerHandler(xxx) 只触发第一个匹配元素的指定事件

(3)trigger(xxx) 会冒泡;
triggerHandler(xxx) 不会冒泡

二、$().trigger()

 $("#one").on("click",function () {console.log("one 被点击了")
 })
  
 $("#one").trigger('click')

作用:
看 一、(1)

源码:

    // 触发 type 事件,data 是自定义事件的额外参数
    // 源码 9014 行
    trigger: function(type, data) {return this.each( function() {jQuery.event.trigger( type, data, this);
      } );
    },

解析:
本质是调用的 jQuery.event.trigger() 方法

三、jQuery.event.trigger()

源码:

    // 源码 8850 行
    //type, data, this
    trigger: function(event, data, elem, onlyHandlers) {
      var i, cur, tmp, bubbleType, ontype, handle, special, lastElement,
        // 冒泡路径数组
        eventPath = [elem || document],
        // 判断 event 是否有 'type' 属性,有则取 event.type,没有则取 event
        type = hasOwn.call(event, "type") ? event.type : event,
        // 同上
        namespaces = hasOwn.call(event, "namespace") ? event.namespace.split(".") : [];

      // 当前元素
      cur = lastElement = tmp = elem = elem || document;
      // 文本内容或者是注释则不触发事件
      // Don't do events on text and comment nodes
      if (elem.nodeType === 3 || elem.nodeType === 8) {return;}
      // 由 focus/blur 转变到 focusin/out,现在不触发 focus/blur 事件
      // focus/blur morphs to focusin/out; ensure we're not firing them right now

      //rfocusMorph:focusin focus|focusout blur
      if (rfocusMorph.test( type + jQuery.event.triggered) ) {return;}
      // 可以不看
      if (type.indexOf( ".") > -1 ) {// Namespaced trigger; create a regexp to match event type in handle()
        namespaces = type.split(".");
        type = namespaces.shift();
        namespaces.sort();}
      //onclick,onfocus 等等
      ontype = type.indexOf(":") < 0 && "on" + type;
      //event 一般是字符串,所以一般是 undefined
      // 获取对应 type 类型的 jQuery.event
      // Caller can pass in a jQuery.Event object, Object, or just an event type string
      event = event[jQuery.expando] ?
        event :
        //click,false
        new jQuery.Event(type, typeof event === "object" && event);

      // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
      //onlyHandlers 一般为 undefined
      //3
      event.isTrigger = onlyHandlers ? 2 : 3;
      //""event.namespace = namespaces.join(".");
      //null
      event.rnamespace = event.namespace ?
        new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
        null;
      // 清空 event 以防它被复用
      // Clean up the event in case it is being reused
      event.result = undefined;
      //target 属性为目标 DOM 元素
      // 我们一般取的 e.target.value,也正是目标元素的值
      if (!event.target) {event.target = elem;}
      // 复制 data 并预先考虑 event,创建 handler 集合
      // Clone any incoming data and prepend the event, creating the handler arg list

      // 简单点,就是 data=[event]
      data = data == null ?
        [event] :
        jQuery.makeArray(data, [ event] );

      // 赋值有需要特殊处理的 type
      // Allow special events to draw outside the lines
      special = jQuery.event.special[type] || {};

      if (!onlyHandlers && special.trigger && special.trigger.apply( elem, data) === false ) {return;}

      // 提前确定事件冒泡的路径
      // Determine event propagation path in advance, per W3C events spec (#9951)
      // 冒泡至 document,再到 window;关注全局的 ownerDocument
      // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
      if (!onlyHandlers && !special.noBubble && !isWindow( elem) ) {
        //click
        bubbleType = special.delegateType || type;

        //clickclick
        // 如果不是 focus/blur 的话,获取它的父元素
        if (!rfocusMorph.test( bubbleType + type) ) {cur = cur.parentNode;}
        //for 循环的语法(a; b; c)// a 在单次循环开始前执行
        // b 是单次循环的条件(这里即 cur 存在)// c 是单次循环结束后执行
        for (; cur; cur = cur.parentNode) {console.log(cur,'cur8967')
          // 将目标元素的祖先元素都 push 进数组
          eventPath.push(cur);
          tmp = cur;
        }
        // 只有当 tmp 是 document 时,将 window 加上
        // Only add window if we got to document (e.g., not plain obj or detached DOM)
        if (tmp === ( elem.ownerDocument || document) ) {eventPath.push( tmp.defaultView || tmp.parentWindow || window);
        }
      }
      // 触发冒泡机制
      // Fire handlers on the event path
      i = 0;
      //e.stopPropagation()这是阻止冒泡的方法
      //isPropagationStopped() 检查是否阻止冒泡了,返回 boolean
      while (( cur = eventPath[ i++] ) && !event.isPropagationStopped()) {
        lastElement = cur;
        event.type = i > 1 ?
          bubbleType :
          special.bindType || type;
        console.log(i,'lastElement8987')
        // jQuery handler
        //(dataPriv.get( cur, "events") || {})[event.type]
        // 先判断 cur 元素的 events 是否有绑定 click
        //dataPriv.get(cur, "handle") 
        // 再获取 cur 元素的 click 事件处理程序
        // 获取目标元素的触发事件的事件处理程序
        handle = (dataPriv.get( cur, "events") || {})[event.type] &&
          // 获取触发事件的处理程序
          dataPriv.get(cur, "handle");
        /* 让冒泡元素执行 handle, 这行代码是触发冒泡机制的关键 */
        /* 在执行 click 事件的处理程序后,自然就会执行 e.stopPropagation(),* 从而让 event.isPropagationStopped()=true*/
        if (handle) {handle.apply( cur, data);
        }
        // 接下来处理原生的事件及处理程序
        //click 为 onclick
        // Native handler
        handle = ontype && cur[ontype];
        // 如果有绑定原生 onclick 事件的话
        if (handle && handle.apply && acceptData( cur) ) {
          // 执行 onclick 事件的处理程序
          event.result = handle.apply(cur, data);
          if (event.result === false) {
            // 阻止元素的默认行为(如提交表单 submit)event.preventDefault();}
        }
      }
      
      event.type = type;
      // 如果没有人阻止默认行为的话,现在就阻止
      /* 比如触发 <a> 的 click 事件,但不会跳转 */
      // If nobody prevented the default action, do it now
      if (!onlyHandlers && !event.isDefaultPrevented() ) {
        if ( ( !special._default ||
          special._default.apply(eventPath.pop(), data ) === false ) &&
          acceptData(elem) ) {
          // 在目标上,用重复的命名调用原生 DOM 事件,会在 window 层面上影响其他元素
          // Call a native DOM method on the target with the same name as the event.
          // Don't do default actions on window, that's where global variables be (#6170)
          if (ontype && isFunction( elem[ type] ) && !isWindow(elem) ) {
            // 当我们触发 FOO 事件(如 click)时,不要重复触发它的 onFOO(onclick)事件
            // Don't re-trigger an onFOO event when we call its FOO() method
            tmp = elem[ontype];
            // 将 jQuery 对象的 onclick 属性置为 null
            // 比如 <a> 就不会去跳转了
            if (tmp) {elem[ ontype] = null;
            }
            // 阻止重复触发同样的事件,因为我们已经把它冒泡了
            // Prevent re-triggering of the same event, since we already bubbled it above
            jQuery.event.triggered = type;
            // 如果已经执行阻止冒泡了,则为 window 添加阻止冒泡的监听
            if (event.isPropagationStopped() ) {lastElement.addEventListener( type, stopPropagationCallback);
            }
            console.log(elem[ type],'type9053')
            // 执行 type 事件
            elem[type]();
            if (event.isPropagationStopped() ) {lastElement.removeEventListener( type, stopPropagationCallback);
            }

            jQuery.event.triggered = undefined;

            if (tmp) {elem[ ontype] = tmp;
            }

          }
        }
      }
      return event.result;
    },

解析:

(1)trigger()的冒泡机制的实现

if (!onlyHandlers && !special.noBubble && !isWindow( elem) ) 中,通过 eventPath 存储目标元素的祖先元素:

        //clickclick
        // 如果不是 focus/blur 的话,获取它的父元素
        if (!rfocusMorph.test( bubbleType + type) ) {cur = cur.parentNode;}
        //for 循环的语法(a; b; c)// a 在单次循环开始前执行
        // b 是单次循环的条件(这里即 cur 存在)// c 是单次循环结束后执行
        for (; cur; cur = cur.parentNode) {console.log(cur,'cur8967')
          // 将目标元素的祖先元素都 push 进数组
          eventPath.push(cur);
          tmp = cur;
        }
        // 只有当 tmp 是 document 时,将 window 加上
        // Only add window if we got to document (e.g., not plain obj or detached DOM)
        if (tmp === ( elem.ownerDocument || document) ) {eventPath.push( tmp.defaultView || tmp.parentWindow || window);
        }

通过 eventPath.push(cur. parentNode) 将冒泡元素装进数组中,并通过 while 循环触发 冒泡机制

      // 触发冒泡机制
      // Fire handlers on the event path
      i = 0;
      //e.stopPropagation()这是阻止冒泡的方法
      //isPropagationStopped() 检查是否阻止冒泡了,返回 boolean
      while (( cur = eventPath[ i++] ) && !event.isPropagationStopped()) {
        lastElement = cur;
        event.type = i > 1 ?
          bubbleType :
          special.bindType || type;
        console.log(i,'lastElement8987')
        // jQuery handler
        //(dataPriv.get( cur, "events") || {})[event.type]
        // 先判断 cur 元素的 events 是否有绑定 click
        //dataPriv.get(cur, "handle") 
        // 再获取 cur 元素的 click 事件处理程序
        // 获取目标元素的触发事件的事件处理程序
        handle = (dataPriv.get( cur, "events") || {})[event.type] &&
          // 获取触发事件的处理程序
          dataPriv.get(cur, "handle");
        /* 让冒泡元素执行 handle, 这行代码是触发冒泡机制的关键 */
        /* 在执行 click 事件的处理程序后,自然就会执行 e.stopPropagation(),* 从而让 event.isPropagationStopped()=true*/
        if (handle) {handle.apply( cur, data);
        }
        // 接下来处理原生的事件及处理程序
        //click 为 onclick
        // Native handler
        handle = ontype && cur[ontype];
        // 如果有绑定原生 onclick 事件的话
        if (handle && handle.apply && acceptData( cur) ) {
          // 执行 onclick 事件的处理程序
          event.result = handle.apply(cur, data);
          if (event.result === false) {
            // 阻止元素的默认行为(如提交表单 submit)event.preventDefault();}
        }
      }

关键代码是handle.apply(cur, data),它用来执行 cur 元素的事件的处理程序。

(2)通过 e.stopPropagation() 来阻止冒泡的原理:

<body>
<script src="jQuery.js"></script>
<div id="one"> 这是 one</div>
<script>
    $("#one").click(function(e){// 将 handle.apply( cur, data); 注释后,冒泡不生效
      e.stopPropagation()
      console.log('one 被点击了')
    })

    $("body").click(function(){console.log('body 被点击了')
    })
    // 执行 trigger()后,会打印 one 被点击了和 body 被点击了
    $("#one").trigger('click')
</script>
</body>

① 上面这段代码会先执行$("#one").trigger('click')

② trigger()里会执行到上面(1)的 handle.apply(cur, data);,而handle 会执行 $("#one")click事件的处理程序:

      e.stopPropagation()
      console.log('one 被点击了')

e.stopPropagation()走的是这里:

  //event 的属性赋值
  // 源码 5749 行
  jQuery.Event.prototype = {
    constructor: jQuery.Event,
    //xxx
    isPropagationStopped: returnFalse, //false
    //xxx
    //xxx
    // 当执行 e.stopPropagation()后走这边
    // 源码 5767 行
    stopPropagation: function() {
      var e = this.originalEvent;
      //isPropagationStopped 方法返回 true
      this.isPropagationStopped = returnTrue;

      if (e && !this.isSimulated) {e.stopPropagation();
      }
    },
}

最后让 isPropagationStopped() 方法返回true

④ 注意:$().trigger()里的 event 也就是 click 里的 event,所以会影响到while 循环地判断,从而达到阻止冒泡循环的 目的

while (( cur = eventPath[ i++] ) && !event.isPropagationStopped()) {}

⑤ 为什么说 click 里的 event$().trigger()里的event

     //event 一般是字符串,所以一般是 undefined
      // 获取对应 type 类型的 jQuery.event
      // Caller can pass in a jQuery.Event object, Object, or just an event type string
      event = event[jQuery.expando] ?
        event :
        //click,false
        new jQuery.Event(type, typeof event === "object" && event);

因为 event 是根据 type(click) 类型生成的,所以 trigger 里的 event 的部分属性和 clickevent属性相同。

(3)原生 js 绑定的事件的执行,如onclick

    $("#one").click(function(e){console.log('one 被点击了')
    })

    document.getElementById("one").onclick=function(){console.log('onclick 被点击了')
    }

还是在 while 循环中:

        // 接下来处理原生的事件及处理程序
        //click 为 onclick
        // Native handler
        handle = ontype && cur[ontype];
        // 如果有绑定原生 onclick 事件的话
        if (handle && handle.apply && acceptData( cur) ) {
          // 执行 onclick 事件的处理程序
          event.result = handle.apply(cur, data);
          if (event.result === false) {
            // 阻止元素的默认行为(如提交表单 submit)event.preventDefault();}
        }

也就是说:
在冒泡循环机制中,在执行完 jQuery 绑定的 handler 后,会接着执行原生 JS 绑定的handler

(4)rfocusMorph

  // 匹配 focusinfocus 或者 focusoutblur
  // 源码 8872 行
  var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,

(5)jQuery.makeArray()

作用:
用于将一个类似数组的对象转换为真正的数组对象

注意:
类数组对象具有许多数组的属性 (例如 length 属性,[] 数组访问运算符等),不过它毕竟不是数组,缺少从数组的原型对象上继承下来的内置方法 (例如:pop()、reverse() 等)。

源码:

    // 结果仅供内部使用
    // results is for internal usage only
    // 源码 442 行
    makeArray: function(arr, results) {var ret = results || [];

      if (arr != null) {//Object()等效于 new Object()
        // 先将 arr 转为对象类型,因为 js 中的 array 是 Object
        if (isArrayLike( Object( arr) ) ) {
          // 将 second 合并到 first 后面
          jQuery.merge( ret,
            typeof arr === "string" ?
              [arr] : arr
          );
        } else {//ret.push(arr)
          push.call(ret, arr);
        }
      }
      // 返回 array
      return ret;
    },

$.isArrayLike

作用:
判断是不是类数组

源码:

  // 判断是不是类数组
  // 源码 561 行
  function isArrayLike(obj) {// Support: real iOS 8.2 only (not reproducible in simulator)
    // `in` check used to prevent JIT error (gh-2145)
    // hasOwn isn't used here due to false negatives
    // regarding Nodelist length in IE
    // 后两个是兼容性考虑的判断
    var length = !!obj && "length" in obj && obj.length,
      //obj 类型
      type = toType(obj);

    if (isFunction( obj) || isWindow(obj) ) {return false;}

    return type === "array" || length === 0 ||
      typeof length === "number" && length > 0 && (length - 1) in obj;
  }

(6)最后一个 if,触发 trigger()时,阻止 jQuery 元素的默认行为

if (!onlyHandlers && !event.isDefaultPrevented() ){
xxx
xxx
}

综上,trigger 一共做了三件事:

(1)触发冒泡机制
(2)触发原生绑定事件
(3)阻止元素默认行为

最后,附上自己整理的触发 trigger() 的流程图:


(完)

正文完
 0