近幾年來,在移動端上因原生開發成本高和效率低而致使涌現出來的一大批優秀前端框架,以及專門針對移動端設備的前端開發框架(如 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事件機制