深入理解e.target與e.currentTarget

草榴社群發表於2017-10-26

target與currentTarget兩者既有區別,也有聯絡,那麼我們就來探討下他們的區別吧,一個通俗易懂的例子解釋一下兩者的區別:


 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <title>Example</title>
 5 </head>
 6 <body>
 7     <div id="A">
 8         <div id="B">
 9         </div>
10     </div>
11 </body>
12 </html>複製程式碼

var a = document.getElementById('A'),
      b = document.getElementById('B');    
function handler (e) {
    console.log(e.target);
    console.log(e.currentTarget);
}
a.addEventListener('click', handler, false);複製程式碼

當點選A時:輸出:

1 <div id="A">...<div>
2 <div id="A">...<div>複製程式碼

當點選B時:輸出:

1 <div id="B"></div>
2 <div id="A">...</div>複製程式碼

也就是說,currentTarget始終是監聽事件者,而target是事件的真正發出者

由於要相容IE瀏覽器,所以一般都在冒泡階段來處理事件,此時target和currentTarget有些情況下是不一樣的。

如:


1 function(e){
2     var target = e.target || e.srcElement;//相容ie7,8
3     if(target){
4         zIndex = $(target).zIndex();
5     }
6 }
7 
8 //往上追查呼叫處
9 enterprise.on(img,'click',enterprise.help.showHelp);複製程式碼

IE7-8下使用$(target).zIndex();可以獲取到
IE7-8下使用$(e.currentTarget).zIndex();獲取不到,可能是IE下既沒有target,也沒有currentTarget

再來證實一下猜測,在IE瀏覽器下執行以下程式碼:


1 <input type="button" id="btn1" value="我是按鈕" />
2 <script type="text/javascript"> 
3     btn1.attachEvent("onclick",function(e){
4         alert(e.currentTarget);//undefined
5         alert(e.target);       //undefined
6         alert(e.srcElement);   //[object HTMLInputElement]
7     });
8 </script>複製程式碼

物件this、currentTarget和target

在事件處理程式內部,物件this始終等於currentTarget的值,而target則只包含事件的實際目標。如果直接將事件處理程式指定給了目標元素,則this、currentTarget和target包含相同的值。來看下面的例子:

1 var btn = document.getElementById("myBtn");
2 btn.onclick = function (event) {
3     alert(event.currentTarget === this); //ture
4     alert(event.target === this); //ture
5 };複製程式碼

這個例子檢測了currentTarget和target與this的值。由於click事件的目標是按鈕,一次這三個值是相等的。如果事件處理程式存在於按鈕的父節點中,那麼這些值是不相同的。再看下面的例子:

1 document.body.onclick = function (event) {
2     alert(event.currentTarget === document.body); //ture
3     alert(this === document.body); //ture
4     alert(event.target === document.getElementById("myBtn")); //ture
5 };複製程式碼

當單擊這個例子中的按鈕時,this和currentTarget都等於document.body,因為事件處理程式是註冊到這個元素的。然而,target元素卻等於按鈕元素,以為它是click事件真正的目標。由於按鈕上並沒有註冊事件處理程式,結果click事件就冒泡到了document.body,在那裡事件才得到了處理。

在需要通過一個函式處理多個事件時,可以使用type屬性。例如:


 1 var btn = document.getElementById("myBtn");
 2 var handler = function (event) {
 3         switch (event.type) {
 4         case "click":
 5             alert("Clicked");
 6             break;
 7         case "mouseover":
 8             event.target.style.backgroundColor = "red";
 9             bread;
10         case "mouseout":
11             event.target.style.backgroundColor = "";
12             break;
13         }
14     };
15 btn.onclick = handler;
16 btn.onmouseover = handler;
17 btn.onmouseout = handler;複製程式碼

深入理解e.target與e.currentTarget

我們知道一個HTML檔案其實就是一棵DOM樹,DOM節點之間是父子層級關係(這與iOS中的view樹很類似,後面會說到)。在W3C模型中,任何事件發生時,先從頂層開始進行事件捕獲,直到事件觸發到達了事件源元素,這個過程叫做事件捕獲(這其實也是事件的傳遞過程);然後,該事件會隨著DOM樹的層級路徑,由子節點向父節點進行層層傳遞,直至到達document,這個過程叫做事件冒泡(也可以說這是事件的響應過程)。雖然大部分的瀏覽器都遵循著標準,但是在IE瀏覽器中,事件流卻是非標準的。而IE中事件流只有兩個階段:處於目標階段,冒泡階段。


深入理解e.target與e.currentTarget

對於標準事件,事件觸發一次經歷三個階段,所以我們在一個元素上註冊事件也就可以在對應階段繫結事件,移除事件也同樣。


什麼是事件委託呢
事件委託就是利用事件冒泡機制,指定一個事件處理程式,來管理某一型別的所有事件。這個事件委託的定義不夠簡單明瞭,可能有些人還是無法明白事件委託到底是啥玩意。查了網上很多大牛在講解事件委託的時候都用到了取快遞這個例子來解釋事件委託,不過想想這個例子真的是相當恰當和形象的,所以就直接拿這個例子來解釋一下事件委託到底是什麼意思:
公司的員工們經常會收到快遞。為了方便籤收快遞,有兩種辦法:一種是快遞到了之後收件人各自去拿快遞;另一種是委託前臺MM代為簽收,前臺MM收到快遞後會按照要求進行簽收。很顯然,第二種方案更為方便高效,同時這種方案還有一種優勢,那就是即使有新員工入職,前臺的MM都可以代替新員工簽收快遞。
這個例子之所以非常恰當形象,是因為這個例子包含了委託的兩層意思:
首先,現在公司裡的員工可以委託前臺MM代為簽收快遞,即程式中現有的dom節點是有事件的並可以進行事件委託;其次,新入職的新員工也可以讓前臺MM代為簽收快遞,即程式中新新增的dom節點也是有事件的,並且也能委託處理事件。

為什麼要用事件委託呢
當dom需要處理事件時,我們可以直接給dom新增事件處理程式,那麼當許多dom都需要處理事件呢?比如一個ul中有100li,每個li都需要處理click事件,那我們可以遍歷所有li,給它們新增事件處理程式,但是這樣做會有什麼影響呢?我們知道新增到頁面上的事件處理程式的數量將直接影響到頁面的整體執行效能,因為這需要不停地與dom節點進行互動,訪問dom的次數越多,引起瀏覽器重繪和重排的次數就越多,自然會延長頁面的互動就緒時間,這也是為什麼可以減少dom操作來優化頁面的執行效能;而如果使用委託,我們可以將事件的操作統一放在js程式碼裡,這樣與dom的操作就可以減少到一次,大大減少與dom節點的互動次數提高效能。同時,將事件的操作進行統一管理也能節約記憶體,因為每個js函式都是一個物件,自然就會佔用記憶體,給dom節點新增的事件處理程式越多,物件越多,佔用的記憶體也就越多;而使用委託,我們就可以只在dom節點的父級新增事件處理程式,那麼自然也就節省了很多記憶體,效能也更好。
事件委託怎麼實現呢?因為冒泡機制,既然點選子元素時,也會觸發父元素的點選事件。那麼我們就可以把點選子元素的事件要做的事情,交給最外層的父元素來做,讓事件冒泡到最外層的dom節點上觸發事件處理程式,這就是事件委託。
在介紹事件委託的方法之前,我們先來看看處理事件的一般方法:

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");

item1.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item1");
}
item2.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item2");
}
item3.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item3");
}
</script>複製程式碼

上面的程式碼意思很簡單,就是給列表中每個li節點繫結點選事件,點選li的時候,需要找一次目標li的位置,執行事件處理函式。
那麼我們用事件委託的方式會怎麼做呢?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");
var list = document.getElementById("list");
list.addEventListener("click",function(event){
 var target = event.target;
 if(target == item1){
    alert(event.target.nodeName);
    console.log("hello item1");
 }else if(target == item2){
    alert(event.target.nodeName);
    console.log("hello item2");
 }else if(target == item3){
    alert(event.target.nodeName);
    console.log("hello item3");
 }
});
</script>複製程式碼

我們為父節點新增一個click事件,當子節點被點選的時候,click事件會從子節點開始向上冒泡。父節點捕獲到事件之後,通過判斷event.target來判斷是否為我們需要處理的節點, 從而可以獲取到相應的資訊,並作處理。很顯然,使用事件委託的方法可以極大地降低程式碼的複雜度,同時減小出錯的可能性。
我們再來看看當我們動態地新增dom時,使用事件委託會帶來哪些優勢?首先我們看看正常寫法:

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

var item = list.getElementsByTagName("li");
for(var i=0;i<item.length;i++){
    (function(i){
        item[i].onclick = function(){
            alert(item[i].innerHTML);
        }
    })(i);
}

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>複製程式碼

點選item1到item3都有事件響應,但是點選item4時,沒有事件響應。說明傳統的事件繫結無法對動態新增的元素而動態的新增事件。
而如果使用事件委託的方法又會怎樣呢?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

document.addEventListener("click",function(event){
    var target = event.target;
    if(target.nodeName == "LI"){
        alert(target.innerHTML);
    }
});

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>複製程式碼

當點選item4時,item4有事件響應,這說明事件委託可以為新新增的DOM元素動態地新增事件。我們可以發現,當用事件委託的時候,根本就不需要去遍歷元素的子節點,只需要給父級元素新增事件就好了,其他的都是在js裡面的執行,這樣可以大大地減少dom操作,這就是事件委託的精髓所在。

移動端篇(iOS)

在網頁上當我們講到事件,我們會講到事件的捕獲以及傳遞方式(冒泡),那麼在移動端上,其實也離不開這幾個問題,下面我們將從這幾個方面來介紹iOS的事件機制: 1、 如何找到最合適的控制元件來處理事件?
2、找到事件第一個響應者之後,事件是如何響應的?

一、事件的產生和傳遞

iOS中的事件可以分為3大型別:

  • 觸控事件
  • 加速計事件
  • 遠端控制事件

這裡我們只討論iOS中最為常見的觸控事件。

響應者物件

學習觸控事件之前,我們需要了解一個比較重要的概念:響應者(UIResponder)。
在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接受並處理事件,我們稱之為“響應者物件”。
之所以繼承自UIResponder的類就能夠接收並處理觸控事件,是因為UIResponder提供了下列屬性和方法來處理觸控事件:

- (nullable UIResponder*)nextResponder;
- (BOOL)canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;
- (BOOL)canResignFirstResponder;    // default is YES
- (BOOL)resignFirstResponder;
- (BOOL)isFirstResponder;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;複製程式碼

當觸控事件產生時,系統在會在觸控的不同階段呼叫上面4個方法。

事件的產生

  • 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的事件佇列中。
  • UIApplication會從事件佇列中取出最前面的事件,並將事件分發下去,首先傳送事件給應用程式的主視窗(keyWindow)。
  • 主視窗會在檢視層次結構中找到一個最合適的檢視來處理觸控事件。
  • 找到合適的檢視控制元件後,就會呼叫檢視控制元件的touches方法來作具體的事件處理。

事件的傳遞

我們的app中,所有的檢視都是按照一定的結構組織起來的,即樹狀層次結構,每個view都有自己的superView,包括controller的topmost view(controller的self.view)。當一個view被add到superView上的時候,他的nextResponder屬性就會被指向它的superView,當controller被初始化的時候,self.view(topmost view)的nextResponder會被指向所在的controller,而controller的nextResponder會被指向self.view的superView。具體的檢視結構如下:


深入理解e.target與e.currentTarget

應用如何找到最合適的控制元件來處理事件

  • 首先判斷當前控制元件自己是否能接受觸控事件;
  • 判斷觸控點是否在自己身上;
  • 在當前控制元件的子控制元件陣列中從後往前遍歷子控制元件,重複前面的兩個步驟(所謂從後往前遍歷子控制元件,就是首先查詢子控制元件陣列中最後一個元素,然後執行1、2步驟);
  • 在上述過程中找到了合適的view,比如叫做fitView,那麼會把這個事件交給這個fitView,再遍歷這個fitView的子控制元件,直至沒有更合適的view為止;
  • 如果沒有符合條件的子控制元件,那麼就認為自己最合適處理這個事件,也就是自己是最合適的view。

在這個尋找最合適的響應控制元件的過程中,所有參與遍歷的控制元件都會呼叫以下兩個方法來確定控制元件是否是更合適的響應控制元件:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event複製程式碼

具體原理可參考:iOS 事件傳遞 hitTest方法與PointInside方法

二、事件的響應

響應者鏈

在iOS檢視中所有控制元件都是按一定層級結構進行組織的,也就是說控制元件是有先後擺放順序的,而能夠響應事件的控制元件按照這種先後關係構成一個鏈條就叫“響應者鏈”。也可以說,響應者鏈是由多個響應者物件連線起來的鏈條。前面提到UIResponder是所有響應者物件的基類,在UIResponder類中定義了處理各種事件的介面。而UIApplication、 UIViewController、UIWindow和所有繼承自UIView的UIKit類都直接或間接的繼承自UIResponder,所以它們的例項都是可以構成響應者鏈的響應者物件。
在iOS中響應者鏈的關係可以用下圖表示:


深入理解e.target與e.currentTarget

下面我們根據響應者鏈關係圖解釋事件傳遞過程:

  • 如果當前view是控制器的view,那麼控制器(viewController)就是上一個響應者,事件就傳遞給控制器;如果當前view不是控制器的view,那麼父檢視就是當前view的上一個響應者,事件就傳遞給它的父檢視;
  • 如果檢視層次結構的最頂級檢視也不能處理收到的事件,則其將事件傳遞給window物件進行處理;
  • 如果window物件不能處理該事件,則其將事件傳遞給UIApplication物件;
  • 如果UIApplication也不能處理該事件,則將其丟棄。

當檢視響應觸控事件時,會自動呼叫自己的touches方法處理事件:

#import "DYView.h"
@implementation DYView
    //只要點選控制元件,就會呼叫touchBegin,如果沒有重寫這個方法,就不能響應處理觸控事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    ...
    // 預設會把事件傳遞給上一個響應者,上一個響應者是父控制元件,交給父控制元件處理
    [super touchesBegan:touches withEvent:event];
    // 注意不是呼叫父控制元件的touches方法,而是呼叫父類的touches方法,最終會把事件傳遞給nextResponder
}
@end複製程式碼

無論當前子控制元件能否處理事件,都會把事件上拋給父控制元件(上一個響應者),如果父控制元件實現了touches方法,則會處理觸控事件,也就是說一個觸控事件可以只由一個控制元件進行處理,也可以由多個控制元件進行響應處理。所以, 整個觸控事件的傳遞和響應過程可概括如下:

  • 當一個事件發生後,事件會由UIApplication沿著傳遞鏈分發下去,即UIApplication -> UIWindow -> UIView -> initial view,直到尋找最合適的view。
  • 最合適的view之後開始響應事件:首先看initial view能否處理這個事件,如果不能則會將事件傳遞給其上級檢視;如果上級檢視仍然無法處理則會繼續往上傳遞;一直傳遞到檢視控制器view controller;如果不能則接著判斷該檢視控制器能否處理此事件,如果還是不能則繼續向上傳 遞;(對於第二個圖檢視控制器本身還在另一個檢視控制器中,則繼續交給父檢視控制器的根檢視,如果根檢視不能處理則交給父檢視控制器處理);一直到 window,如果window還是不能處理此事件則繼續交給application處理,如果最後application還是不能處理此事件則將其丟棄。
  • 在事件的響應中,如果某個控制元件實現了touches方法,則這個事件將由該控制元件來接受,如果呼叫了[super touches….];就會將事件順著響應者鏈條往上傳遞,傳遞給上一個響應者;接著就會呼叫上一個響應者的touches方法。

三、事件繫結和事件代理

事件繫結

在iOS應用開發中,經常會用到各種各樣的控制元件,比如按鈕(UIButton)、開關(UISwitch)、滑塊(UISlider)等以及各種自定義控制元件。這些控制元件用來與使用者進行互動,響應使用者的操作。這些控制元件有個共同點,它們都是繼承於UIControl類。UIControl是控制元件類的基類,它是一個抽象基類,我們不能直接使用UIControl類來例項化控制元件,它只是為控制元件子類定義一些通用的介面,並提供一些基礎實現,以在事件發生時,預處理這些訊息並將它們傳送到指定目標物件上。
iOS中的事件繫結是一種Target-Action機制,其操作主要使用以下兩個方法:

// 新增繫結
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
// 解除繫結
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents複製程式碼

當我們需要給一個控制元件(例如按鈕)繫結一個點選事件時,可做如下處理:

[button addTarget:self action:@selector(clickButton:) forControlEvents:UIControlEventTouchUpInside];複製程式碼

當按鈕的點選事件發生時,訊息會被髮送給target(這裡即為self物件),觸發target物件的clickButton:方法來處理點選點選事件。這個過程可用下圖來描述:


深入理解e.target與e.currentTarget

因此,Target-Action機制由兩部分組成:即目標物件和行為Selector。目標物件指定最終處理事件的物件,而行為Selector則是處理事件的方法。
如果目標物件target為空會怎樣呢?如果我們沒有指定target,則會將事件分發到響應鏈上第一個想處理訊息的物件上,也就是根據響應者鏈往上找,若找到,則呼叫,否則什麼也不做。例如下面的程式碼:

[button addTarget:nil action:@selector(clickButton:) forControlEvents:UIControlEventTouchUpInside];複製程式碼

上面的程式碼目標物件為nil,那麼它首先會檢查button自身這個類有沒有實現clickButton:這個方法,如果實現了這個方法就會呼叫,否則就會根據響應者鏈找到button.nextResponder,再次檢查是否實現了clickButton:方法,直到UIApplication(其實是AppDelegate),如果還是沒有實現,則什麼也不做。

事件代理

在IOS中委託通過一種@protocol的方式實現,所以又稱為協議.協議是多個類共享的一個方法列表,在協議中所列出的方法沒有相應的具體實現(相當於介面),需要由使用協議的類來實現協議中的方法。
委託是指給一個物件提供機會對另一個物件中的變化做出反應或者影響另一個物件的行為。其基本思想是:兩個物件協同解決問題。一個物件非常普通,並且打算在廣泛的情形中重用。它儲存指向另一個物件(即它的委託)的引用,並在關鍵時刻給委託發訊息。訊息可能只是通知委託發生了某件事情,給委託提供機會執行額外的處理,或者訊息可能要求委託提供一些關鍵的資訊以控制所發生的事情。
下面用用一個例子來說明代理在iOS開發中的具體應用:
還是以取快遞為例,員工可以委託前臺MM代為簽收快遞,所以員工和前臺MM之間有一個協議(protocol):

@protocol signDelegate <NSObject>
- (void)signTheExpress;
@end複製程式碼

這個協議裡宣告瞭一個簽收快遞(signTheExpress)的方法。
員工可用下面定義的類表示:

##employer.h

@protocol signDelegate <NSObject>
- (void)signTheExpress;
@end

@interface employer : NSObject
/**
 * delegate 是employer類的一個屬性
 */
@property (nonatomic, weak) id<signDelegate> delegate;
- (void)theExpressDidArrive;
@end複製程式碼
employer.m
#import "employer.h"

@implementation employer
- (void)theExpressDidArrive{
    if ([self.delegate respondsToSelector:@selector(signTheExpress)]) {
        [self.delegate signTheExpress];
    }
}
@end複製程式碼

再來看看前臺MM這個類的實現:

#import "receptionMM.h"
#import "employer.h"

@interface receptionMM ()<signDelegate>  //<signDelegate>表示遵守signDelegate協議,並且實現協議裡面的方法

@end

@implementation receptionMM
/**
 * 快遞員到了
 */
- (void)theCourierCome{
    DaChu *employer1 = [[employer alloc] init];
    employer1.delegate = self; //說明前臺MM充當代理的角色。
    [employer1 theExpressDidArrive]; //某個員工的快遞到了
}
- (void)signTheExpress{
    NSLog(@"快遞簽收了");
}
@end複製程式碼

在iOS開發中,使用委託的主要目的在於解耦,因為不同的模組有自己的角色,對於事件的處理需要由特定模組完成以保持資料和UI的分離,同時也能降低程式的複雜度。

總結

雖然前端和移動端的開發存在很大的差異,但僅從事件機制來看,兩者也存在很多相似的概念:例如前端的dom樹的概念和App頁面中的view樹很類似,前端事件的捕獲和冒泡機制和iOS事件的傳遞鏈和響應鏈機制也有相似之處,以及兩端都有事件繫結和事件代理的概念。但由於移動端頁面元素高度物件化的特徵,對於事件的處理機制相對也更復雜一點,一些設計模式的應用的目的有所差異。比如事件委託在前端開發上主要是降低程式碼的複雜度,而在iOS開發上則主要在於解決模組間的耦合問題。並且前端和移動端平臺上也都有許多優秀的框架,因而關於前端和移動端的事件機制還有很多內容可以談,比如Vue.js的Event Bus、ReactiveCocoa中統一的訊息處理機制,希望有時間可以再探討一番。



相關文章