深刻理解e.target與e.currentTarget

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


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時:輸出:html

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

當點擊B時:輸出:前端

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

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

因爲要兼容IE瀏覽器,因此通常都在冒泡階段來處理事件,此時target和currentTarget有些狀況下是不同的。node

如:設計模式


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 bash

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

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;複製代碼



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




對於標準事件,事件觸發一次經歷三個階段,因此咱們在一個元素上註冊事件也就能夠在對應階段綁定事件,移除事件也一樣。


什麼是事件委託呢
事件委託就是利用事件冒泡機制,指定一個事件處理程序,來管理某一類型的全部事件。這個事件委託的定義不夠簡單明瞭,可能有些人仍是沒法明白事件委託究竟是啥玩意。查了網上不少大牛在講解事件委託的時候都用到了取快遞這個例子來解釋事件委託,不過想一想這個例子真的是至關恰當和形象的,因此就直接拿這個例子來解釋一下事件委託究竟是什麼意思:
公司的員工們常常會收到快遞。爲了方便籤收快遞,有兩種辦法:一種是快遞到了以後收件人各自去拿快遞;另外一種是委託前臺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、事件的產生和傳遞

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。具體的視圖結構以下:




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

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

在這個尋找最合適的響應控件的過程當中,全部參與遍歷的控件都會調用如下兩個方法來肯定控件是不是更合適的響應控件:

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

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

2、事件的響應

響應者鏈

在iOS視圖中全部控件都是按必定層級結構進行組織的,也就是說控件是有前後擺放順序的,而可以響應事件的控件按照這種前後關係構成一個鏈條就叫「響應者鏈」。也能夠說,響應者鏈是由多個響應者對象鏈接起來的鏈條。前面提到UIResponder是全部響應者對象的基類,在UIResponder類中定義了處理各類事件的接口。而UIApplication、 UIViewController、UIWindow和全部繼承自UIView的UIKit類都直接或間接的繼承自UIResponder,因此它們的實例都是能夠構成響應者鏈的響應者對象。
在iOS中響應者鏈的關係能夠用下圖表示:




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

  • 若是當前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方法。

3、事件綁定和事件代理

事件綁定

在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:方法來處理點擊點擊事件。這個過程可用下圖來描述:




所以,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中統一的消息處理機制,但願有時間能夠再探討一番。

相關文章
相關標籤/搜索