jQuery源码解析之你并不真的懂事件委托及target和currenttarget的区别

44次阅读

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

前言:
回顾下我之前写的一篇文章:JavaScript 之事件委托

一、事件委托(委派)
含义:
#A 上绑定 click 事件,但是让 #B 触发 click 事件,相当于在 #B 上假绑定了 click 事件

也就是说:#B 委托了 click 事件给了 #A(在 #A 上绑定)

举例:

<div id="A" style="background-color: deeppink">
  这是 A
  
  <div id="B" style="background-color: bisque">
    这是 B
  
    <div id="C" style="background-color: aqua">
    这是 C
    </div>
  
    <div id="D" style="background-color: blueviolet">
    这是 D
    </div>

  </div>
</div>

  // 在父元素上绑定 click 事件,但只能由子元素触发父元素上绑定的事件
  $("#A").on("click" ,"#B",function (e) {console.log("点击了 B,即 B 委托 A 的 click 事件被点击了")
  })
  $("#A").on("click" ,"#C",function (e) {console.log(e,"点击了 C,即 C 委托 A 的 click 事件被点击了")
  })

二、jQuery 的事件委托顺序:

举例:

(1)A、B、C 各自绑定了 click 事件

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

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

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

点击 C,会依次执行 C、B、A 的 click 事件

输出结果:
① C 被点击了
② B 被点击了
③ A 被点击了

(2)A 自己绑定了 click 事件,同时 B、C 还委托给 A 绑定 click 事件

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

 $("#A").on("click" ,"#B",function () {console.log("点击了 B,即 B 委托 A 的 click 事件被点击了")
 })

 $("#A").on("click" ,"#C",function () {console.log("点击了 C,即 C 委托 A 的 click 事件被点击了")
 })

点击 C,依次执行 C、B 委托给 A 的 click 事件,最后执行 A 自己的 click 事件

输出结果:
① 点击了 C,即 C 委托 A 的 click 事件被点击了
② 点击了 B,即 B 委托 A 的 click 事件被点击了
③ A 被点击了

(3)A 自己绑定了 click 事件,同时 B、C 还委托给 A 绑定 click 事件,同时 B、C 还有自己的 click 事件:

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

 $("#A").on("click" ,"#B",function () {console.log("点击了 B,即 B 委托 A 的 click 事件被点击了")
 })

 $("#A").on("click" ,"#C",function () {console.log("点击了 C,即 C 委托 A 的 click 事件被点击了")
 })

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

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

点击 C,依次执行:C 自己的事件、B 自己的事件、C 委托给 A 的 click 事件、B 委托给 A 的 click 事件、A 自己的 click 事件。

输出结果:
① C 被点击了
② B 被点击了
③ 点击了 C,即 C 委托 A 的 click 事件被点击了
④ 点击了 B,即 B 委托 A 的 click 事件被点击了
⑤ A 被点击了

综上,jQuery 事件委托的顺序为:
(1)先统一 处理自身、父元素 自身 绑定的事件
(2) 再统一 处理自身、父元素 委托 给祖先元素的绑定事件
(3) 最后 祖先元素处理 自身 的事件

简练说,就是:
先处理子元素委托给自身的事件,再处理自身的事件。

源码:
$().on()—>jQuery.event.add()

jQuery.event = {
    // 源码 5241 行
    //this, types, fn, data, selector

    //#A,'click',function(){console.log('A 被点击了')},undefined,undefined
    //#A,'click',function(){点击了 C,即 C 委托 A 的 click 事件被点击了},undefined,#C
    add: function(elem, types, handler, data, selector) {
      xxx
      ...
      // 优先添加委托 handler,再添加其他 handler
      // Add to the element's handler list, delegates in front
      //delegateCount 即委托在 #A 上的事件数量
      if (selector) {
        // 在下标为 handlers.delegateCount++ 的位置插入委托事件
        handlers.splice(handlers.delegateCount++, 0, handleObj);
      } else {handlers.push( handleObj);
      }
}

解析:
可以看到,jQuery 是优先添加委托 click 事件,再添加自身 click 事件,触发事件的时候也是按这个顺序。

注意:
如下的例子,点击 E 是不能触发 click 事件的,因为冒泡冒不到 A 上:

<div id="A" style="background-color: deeppink">
  这是 A
</div>

<div id="E" style="background-color: brown"> 这是 E </div>

  $("#A").on("click" ,"#E",function (event) {console.log(event,"点击了 E,即 E 委托 A 的 click 事件被点击了")
  })

三、jQuery 绑定事件上的 event 上的 target、currenttarget 和 delegateTarget 的区别?

target 是触发事件的对象
delegateTarget 是事件委托的 原对象

而 currenttarget 分三种情况:
(1)A 在自身有绑定 click 事件的条件下,C 再去委托 A 绑定 click 事件

<div id="A" style="background-color: deeppink">
  这是 A

  <div id="B" style="background-color: bisque">
    这是 B

    <div id="C" style="background-color: aqua">
    这是 C
    </div>

    <div id="D" style="background-color: blueviolet">
    这是 D
    </div>

  </div>
</div>

  $("#A").on("click" ,function (event) {console.log(event,"A 被点击了")
  })

  $("#A").on("click" ,"#C",function (event) {console.log(event,"点击了 C,即 C 委托 A 的 click 事件被点击了")
  })
  
  $("#C").on("click",function (event) {console.log(event,"C 被点击了")
  })

点击了 C,即 C 委托 A 的 click 事件被点击了
event 的结构如下:

可以看到,
target 是 #C,currenttarget 是 #A,delegateTarget 是 #A

也就是说:
target 是触发 click 事件的对象 #C,currenttarget 是 #C 委托绑定 click 事件的 #A,并且 #A 自身有绑定 click 事件

② A 被点击了
target 是 #A,currenttarget 是 #A,delegateTarget 是 #A

③ C 被点击了
target 是 #C,currenttarget 是 #C,delegateTarget 是 #C


(2)A 自身没有绑定 click 事件,C 委托 A 绑定 click 事件

<div id="A" style="background-color: deeppink">
  这是 A

  <div id="B" style="background-color: bisque">
    这是 B

    <div id="C" style="background-color: aqua">
    这是 C
    </div>

    <div id="D" style="background-color: blueviolet">
    这是 D
    </div>

  </div>
</div>

  $("#A").on("click" ,"#C",function (event) {console.log(event,"点击了 C,即 C 委托 A 的 click 事件被点击了")
  })
  
  $("#C").on("click",function (event) {console.log(event,"C 被点击了")
  })

点击了 C,即 C 委托 A 的 click 事件被点击了
event 的结构如下:

可以看到,
target 是 #C,currenttarget 是 #C,而不是 #A,delegateTarget 是 #A

也就是说:
target 是触发 click 事件的对象 #C,currenttarget 是 #C,因为 #C 委托 #A 绑定 click 事件,并且 #A 自身没有绑定 click 事件

② C 被点击了
target 是 #C,currenttarget 是 #C,delegateTarget 是 #C

(3)A 在自身有绑定 click 事件的条件下,C 再去委托 A 绑定 click 事件的同时,阻止冒泡!

<div id="A" style="background-color: deeppink">
  这是 A

  <div id="B" style="background-color: bisque">
    这是 B

    <div id="C" style="background-color: aqua">
    这是 C
    </div>

    <div id="D" style="background-color: blueviolet">
    这是 D
    </div>

  </div>
</div>

  $("#A").on("click" ,"#C",function (event) {event.stopPropagation()
    console.log(event,"点击了 C,即 C 委托 A 的 click 事件被点击了")
  })

  $("#C").on("click",function (event) {console.log(event,"C 被点击了")
  })

点击了 C,即 C 委托 A 的 click 事件被点击了
event 的结构如下:

可以看到,
target 是 #C,currenttarget 是 #C,而不是 #A,delegateTarget 是 #A

② C 被点击了
target 是 #C,currenttarget 是 #C,delegateTarget 是 #C


为什么是这样?
我们来分析下 jQuery 源码:
$().on()—>jQuery.event.add()—>elem.addEventListener(type, eventHandle)eventHandle—>jQuery.event.dispatch
currenttarget 在 jQuery.event.dispatch 中定义,所以我们看 jQuery.event.dispatch 部分源码:

jQuery.event = {
  // 源码 5472 行
  //nativeEvent 即原生 MouseEvent
  dispatch: function(nativeEvent) {
    // 获取 handler 队列
    handlerQueue = jQuery.event.handlers.call(this, event, handlers);

    // 如果没有阻止冒泡的话,那么
    while (( matched = handlerQueue[ i++] ) && !event.isPropagationStopped()) {event.currentTarget = matched.elem;}
  }
  
    // 源码 5547 行
    // 组装事件处理队列  
    //event 是 fix 过的 MouseEvent, handlers  
    handlers: function(event, handlers) {
      // 目标元素
      var cur = event.target;
      for (; cur !== this; cur = cur.parentNode || this) {if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true) ) {matchedHandlers = [];
            matchedSelectors = {};
            for (i = 0; i < delegateCount; i++) {handleObj = handlers[ i];
              //sel 就是 #C
              // Don't conflict with Object.prototype properties (#13203)
              sel = handleObj.selector + " ";

              if (matchedSelectors[ sel] === undefined ) {matchedSelectors[ sel] = handleObj.needsContext ?
                  jQuery(sel, this).index(cur) > -1 :
                  // 注意:jQuery.find()和 jQuery().find()是不一样的
                  jQuery.find(sel, this, null, [ cur] ).length;
              }

              if (matchedSelectors[ sel] ) {matchedHandlers.push( handleObj);
              }
            }
          }

            if (matchedHandlers.length) {handlerQueue.push( { elem: cur, handlers: matchedHandlers} );
           }
      }

     // Add the remaining (directly-bound) handlers
     //#A 
     cur = this;
     //1<2 true
    //1<1 false
     // 将除委托事件的事件(如自身绑定的事件)放入 handlerQueue 中
     if (delegateCount < handlers.length) {handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount) } );
      }
  }

}

解析:
event.currentTarget—>handlerQueue[i++]—>jQuery.event.handlers

jQuery.event.handlers:
for 循环的意思是:
(1)只要 cur 不等于 this,即 #A,就一直循环
每次循环:
(2)将matchedHandlers 置为 []
(3)循环委托绑定的事件数量
循环委托绑定:
(4)matchedHandlers根据 handleObj.selector 是否有值,pushhandleObj

按照我们的例子来看,当 cur=event.target,cur=#C,然后进入冒泡循环,再进入委托事件循环,
关键是:jQuery.find()
cur=#C 的时候,matchedSelectors[sel]=jQuery.find(sel, this, null, [ cur] ).length=1
但是 cur=#B 的时候(冒泡循环),matchedSelectors[sel]=0,也就是说 jQuery.find() 不同于$().find,它是冒泡找 cur 元素!

所以 matchedHandlers 只 pushlength!==0的委托事件,所以 cur 就是 #C 了(新循环中的当前值)。

然后

cur = this;

cur 又等于 this,即 #A,最后将除委托事件的事件(如自身绑定的事件)放入 handlerQueue 中,cur=#A


再拿例子举,即(2)A 自身没有绑定 click 事件,C 委托 A 绑定 click 事件
只有一个 handler,并且是委托 handler,

handlerQueue[
  {
    elem:#C,
    ...
  },
]
//#C
event.currentTarget = handlerQueue[0].elem

(1)A 在自身有绑定 click 事件的条件下,C 再去委托 A 绑定 click 事件
有两个 handler

handlerQueue[
  {
    elem:#C,
    ...
  },
  {
    elem:#A,
    ...
  },
]
//#C
event.currentTarget = handlerQueue[0].elem
//#A
event.currentTarget = handlerQueue[1].elem

因为 #A 只有一个 event,所以在循环handlerQueue[i] 时,event.currenttarget最终被 #A 所覆盖

 while (( matched = handlerQueue[ i++] ) && !event.isPropagationStopped()) {
        // 最终被 #A 所覆盖
        event.currentTarget = matched.elem;
    }

(3)A 在自身有绑定 click 事件的条件下,C 再去委托 A 绑定 click 事件的同时,阻止冒泡!
因为 !event.isPropagationStopped(),所以event.currentTarget=#C,未被#A 覆盖。


(完)

正文完
 0