近几年来,在移动端上因原生开发成本高和效率低而致使涌现出来的一大批优秀前端框架,以及专门针对移动端设备的前端开发框架(如 RN/Weex),大前端的概念被不断地说起。在这样的背景之下,前端技术也将逐渐成为移动端开发者的必备技能。笔者做为一名移动端开发者,在接触了前端开发以后,发现了虽然前端相较于移动端有着很大的不一样,不过前端有很多值得移动端学习的地方,而且二者在很多方面也有着类似之处。在大前端的话题圈里,有很多共同的话题,例如:MVC和MVVM架构、组件化、响应式编程、工程化(打包工具、包管理工具)等等。笔者打算从前端和移动端(以iOS平台为例)的事件机制谈起,对比两端的实现方法有哪些相同和不一样之处,同时也算是对前端与移动端的事件机制作一些总结吧。html
不管是前端仍是移动端,用户在浏览网页或者APP时,一般会在屏幕上产生不少交互操做,例如点击、选择、滚动屏幕、键盘输入等待,而且网页或APP也会根据不一样的操做进行响应变化。这种基于事件的处理方式,本质上是一种消息传递机制,称之为事件机制。前端
在事件机制中,有3样最重要的东西:node
事件生产者能够产生一系列的事件对象,而后事件对象携带着必要的信息,传递给事件消费者。编程
EMCAScript标准规定事件流包含三个阶段,分别为事件捕获阶段,处于目标阶段,事件冒泡阶段。设计模式
<html>
<body>
<div>
<button id="mybtn" onclick="buttonClickHandler(event)">点我试试</button>
</div>
</body>
</html>
<script>
function buttonClickHandler(event) {
console.log('button clicked')
}
</script>
复制代码
在上面的代码中,若是点击按钮button,则标准事件触发分别经历如下三个阶段:数组
target.addEventListener(type, listener, useCapture);
// 标准注册事件函数
// target:文档节点、document、window 或 XMLHttpRequest。
// 函数的参数:注册事件类型type,事件的回调函数,事件注册在捕获期间仍是冒泡期间
// 例如:给button注册onclick事件,要是在捕获阶段注册,则 button.addEventListener("click",function(){},true);
target.removeEventListener(type, listener, useCapture); //在某一个元素上撤销已注册的事件。
复制代码
下面看一个Chrome浏览器中的例子:浏览器
<html>
<head>
<style>
ul{
background : gray;
padding : 20px;
}
ul li{
background : green;
}
</style>
</head>
<body>
<ul>
<li>点我试试</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.addEventListener('click',function(e){console.log('document clicked')},true);//第三个参数为true使用捕获
ul.addEventListener('click',function(e){console.log('ul clicked')},true);
li.addEventListener('click',function(e){console.log('li clicked')},true);
</script>
</body>
</html>
复制代码
以上代码中,咱们建立了一个列表项,点击“点我试试”,看看会有什么状况发生:bash
document clicked
ul clicked
li clicked
复制代码
在咱们的开发者工具控制台上,能够看到打印出了这样三行结果,这是咱们预料之中的事情,由于在这里事件捕获起了做用,点击事件依次触发了document、ul节点、li节点。前端框架
而在IE中只支持冒泡机制,因此只能在冒泡阶段进行事件绑定以及事件撤销:架构
target.attachEvent(type, listener); //target: 文档节点、document、window 或 XMLHttpRequest。
//函数参数: type:注册事件类型;
// listener:事件触发时的回调函数。
target.detachEvent(type,listener); //参数与注册参数相对应。
复制代码
下面看一个IE浏览器里的例子:
<html>
<body>
<ul>
<li>点我试试</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.attachEvent('onclick',function(event){console.log('document clicked')})
ul.attachEvent('onclick',function(event){console.log('ul clicked')});
li.attachEvent('onclick',function(event){console.log('li clicked')});
</script>
</body>
</html>
复制代码
一样地,咱们点击“点我试试”,开发者工具控制台里打印出了下面的结果:
li clicked
ul clicked
document clicked
复制代码
然而有时候事件的捕获机制以及冒泡机制也会带来反作用,好比冒泡机制会触发父节点上本来并不但愿被触发的监听函数,因此有办法可使得冒泡提早结束吗?咱们只须要在但愿事件中止冒泡的位置,调用event对象的stopPropagation函数(IE浏览器中为cancelBubble)便可终止事件冒泡了。好比在上面IE浏览器中示例代码做以下修改:
li.attachEvent('onclick',function(event){
console.log('li clicked');
event.cancelBubble=true;
});
复制代码
修改后,再次点击“点我试试”,在控制台里只打印出一行结果,ul节点和document不会再接收到冒泡上来的click事件,于是它们注册的事件处理函数也将不会被触发了:
li clicked
复制代码
什么是事件委托呢?
事件委托就是利用事件冒泡机制,指定一个事件处理程序,来管理某一类型的全部事件。这个事件委托的定义不够简单明了,可能有些人仍是没法明白事件委托究竟是啥玩意。查了网上不少大牛在讲解事件委托的时候都用到了取快递这个例子来解释事件委托,不过想一想这个例子真的是至关恰当和形象的,因此就直接拿这个例子来解释一下事件委托究竟是什么意思:
公司的员工们常常会收到快递。为了方便签收快递,有两种办法:一种是快递到了以后收件人各自去拿快递;另外一种是委托前台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中的事件能够分为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个方法。
事件的产生
事件的传递
咱们的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。
应用如何找到最合适的控件来处理事件?
在这个寻找最合适的响应控件的过程当中,全部参与遍历的控件都会调用如下两个方法来肯定控件是不是更合适的响应控件:
- (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中响应者链的关系能够用下图表示:
当视图响应触摸事件时,会自动调用本身的touches方法处理事件:
#import "DYView.h"
@implementation DYView
//只要点击控件,就会调用touchBegin,若是没有重写这个方法,就不能响应处理触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
...
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法,最终会把事件传递给nextResponder
}
@end
复制代码
不管当前子控件可否处理事件,都会把事件上抛给父控件(上一个响应者),若是父控件实现了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:方法来处理点击点击事件。这个过程可用下图来描述:
[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中统一的消息处理机制,但愿有时间能够再探讨一番。
一、iOS事件机制