深刻理解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中统一的消息处理机制,但愿有时间能够再探讨一番。

相关文章
相关标签/搜索