首先需要明確的幾個問題:
Q1、什麼是事件?
A:事件就是一個有名字的行為。當這個行為發生的時候,稱這個事件被觸發。
Q2、監聽器又是什麼?
A:監聽器決定了事件的邏輯表達,由事件觸發。監聽器和事件往往是成對的,當然也可以是一個事件對應多個監聽器。監聽器是對事件的反應。當事件被觸發時,由監聽器做出反應。這樣一來,多個事件的觸發可以導致一個監聽器做出反應。一個事件也可以有多個監聽器做出反應。(一句話:監聽器和事件之間的關系既可以是一對多,也可以是多對一)
Q3、事件管理器又是干嘛的?
A:事件管理器(EventManager),從名字上就可以看出來是管理事件用的。但他怎麼管理呢?事件管理器往往會為多個事件聚合多個監聽器(這裡的事件和監聽器都是不定數【就是可以是一個也可以是多個】)。事件管理器還負責觸發事件。
一般來說我們用對象來表示事件。一個事件對象描述了事件的基本元素,包括何時以及如何觸發這個事件。
關於事件的基本元素:事件名稱、target(觸發事件的對象,一般是事件對象本身)、事件參數。之前我們講過事件相當與一個行為,在程序裡面我們經常使用方法或函數來表示行為。因此事件的參數往往也是函數的參數。
另外關於Shared managers: 之前講過一個事件可以針對多個監聽器。這就是通過Shared managers實現的。EventManager的實現包含(組合)了SharedEventManagerInterface【在構造函數或者setSharedManager裡面使用了代碼注入的方式,詳情可以查看源碼】),而SharedEventManagerInterface描述了一個聚合監聽器的對象,這些監聽器只連接到擁有指定識別符的事件。SharedEventManager並不會觸發事件,他只提供監聽器並連接到事件。EventManger通過查詢SharedEventMangaer來獲取具有特定標識符的監聽器。
EventManager裡面幾個重要的行為:
1、創建事件:創建事件實際上只是創建EventManagerInterface的一個實例
2、觸發事件:一般在事件行為裡面使用trigger觸發,這樣我們執行該行為的時候便可以直接觸發該事件。函數原型:trigger($eventName,$target=null,$argv=[]);$eventName一般為時間行為名(常用__FUNCTION__代替),$target則為事件對象本身可用$this代替,$argv為傳入事件的參數(一般為事件行為的參數)。
當然事件觸發方式不僅僅只有trigger一種,還有triggerUntil,triggerEvent,triggerEventUntil。從名字上我們就可以看出分兩類:trigger和triggerEvent;trigger類只單純的觸發事件,不需要實現創建事件實例只需要一個事件名字就可以了,而trigger不僅觸發事件還順帶著觸發監聽器,需要事件實例。而帶有Until後綴的方法都需要一個回調函數,每一個監聽器的結果都會傳到該回調函數中,如果回調函數返回了一個true的bool值,EventManager必須使監聽器短路。(關於短路見下文的短回路)
更多內容請查看官方API,或者EventMangerInterface的具體注釋。
3、創建監聽器並連接到事件:
監聽器可以通過EventManager創建,也可以通過SharedEventManager創建。兩者都是使用attach方法,但參數有點兒不一樣。
我們先看EventManager的方式:
方法原型:attach($eventName, callable $listener, $priority = 1)
很簡單,我們只需要事件名,還有一個可調用函數,最後是優先級默認為1(zend裡面的自帶事件的優先級多為負數,所以如果你想讓自定義的監聽器優先級比較高,直接賦值一個正數就行了。)
可調用函數也就是我們的監聽器。事件名有個特殊情況:“*”。這類似於正則匹配,將所有的事件都連接到本監聽器中。
我們現在看看SharedEventManager方式:
方法原型:attach($identifier, $eventName, callable $listener, $priority = 1);
與之前唯一不同的地方多了個identifier參數。關於identifier的源碼注釋如下:
used to pull shared signals from SharedEventManagerInterface instance;
用來從SharedEventmanager實例中拉取分享信號。identifier是一個數組,按照我的理解:如果一個事件(注意SharedEventmanager無法創建事件的)定義了identifier,就意味著該事件是可共享的。讓後SharedEventManger實例使用attach創建監聽器的時候傳入identifier參數。EventManager就可以使用identifier參數查詢所有的監聽器。
令人困惑的是既然有了事件名,那就可以通過事件名來查詢相關監聽器,那為何還要多此一舉的添加identifier屬性?我考慮到的是事件繼承問題:假設有一個事件類Foo包含一個事件行為act,SubFoo繼承了Foo類並重寫了裡面的事件行為act。兩個類都的事件行為都具有相同的事件名act。這時候如果通過事件名來查詢監聽器,顯然會有沖突。這時候我們定義identifier[__CLASS__, get_class($this)],並在監聽器中指定identifier為SubFoo,顯然會匹配到SubFoo類中的事件行為act。
以上我們通過SharedEventManager可以監聽多個事件,另外我們還可以通過listener aggregates實現。通過Zend\EventManager\ListenerAggregateInterface,讓一個類監聽多個事件,連接一個或多個實例方法作為監聽器。同樣的該接口也定義了attach(EventManagerInterface $events)和detach(EventManagerInterface $events)。我們在attach的具體實現中使用EventManager的實例的方法attach監聽到多個事件。
use Zend\EventManager\EventInterface; use Zend\EventManager\EventManagerInterface; use Zend\EventManager\ListenerAggregateInterface; use Zend\Log\Logger; class LogEvents implements ListenerAggregateInterface { private $listeners = []; private $log; public function __construct(Logger $log) { $this->log = $log; } public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach('do', [$this, 'log']); $this->listeners[] = $events->attach('doSomethingElse', [$this, 'log']); } public function detach(EventCollection $events) { foreach ($this->listeners as $index => $listener) { $events->detach($listener); unset($this->listeners[$index]); } } public function log(EventInterface $e) { $event = $e->getName(); $params = $e->getParams(); $this->log->info(sprintf('%s: %s', $event, json_encode($params))); } }
使用Aggregate的好處:
1、允許你使用有狀態的監聽器
2、在單一的類中組合多個相近的監聽器,並一次性連接他們
內省監聽器返回的結果
我們有了監聽器,但如何接收他返回的結果呢?EventManager默認實現會返回一個ResponseCollection的實例。這個類繼承於PHP的SplStack。基本結構是一個棧,所以允許你反序遍歷Responses。
ResponseCollection提供了有用的幾個方法:
first(): 獲取第一個結果
last(): 獲取最後一個結果
contains($value): 查看是否棧裡面是否包含某一個值,如果包含則返回true,否則false。
短回路監聽器執行:
什麼叫短回路呢?假設你要做一件事情,直到這件事有了結果,這是一個回路。如果你提前知道了這件事的結果(比如之前做過這件事),那你就沒比要把這件事完完全全的做完,這時候你只需要執行一個短回路。
我們在添加EventManager的時候有一個緩存機制。在一個方法中觸發一個事件,如果我們找到一個緩存結果就直接返回。如果找不到緩存結果,我們就將觸發的事件緩存下來以備後用。實際上和計算機硬件裡面的高速緩存一個道理。
EventManager組件提供兩種處理的方式:1、triggerUntil();2、triggerEventUntil。這兩個方法都接受一個回調函數作為第一個參數。如果回調函數返回true,那執行停止。
public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria2'); $results = $this->getEventManager()->triggerUntil( function($r){ return ($r instanceof SomeResultClass); }, __FUNCTION__, $this, $params ); if($results->stopped()) { return $results->last()' } }
從上面范例中,我們知道,如果執行停止了很有可能是因為棧裡面最後的結果滿足我們的要求。這樣一來,我們只要返回該結果,何必還要進行多余的計算呢?
處理在事件中停止執行,我們還可以在監聽器中停止執行。理由是我們曾經接收過某一個事件,現在我們又接收到了相同事件,理所當然的使用之前的結果就好了。這種情況下,監聽器調用stopPropagation(true),然後EventManager會直接返回而不會繼續通知額外的監聽器。
$events->attach('do', function($e) { $e->stopPropagation(); return new SomeResultClass(); });
當然,使用觸發器范例可能會導致歧義,畢竟你並不知道最終的結果是否滿足要求。
Keeping it in order.
偶爾你會關心監聽器的執行順序。我們通過監聽器的優先級來控制執行順序(上面說講的短回路也會影響執行順序)。每一個EventManager::attach()和SharedEventManager::attach()都會接受一個而外的參數:priority。默認情況下為1,我們可以省略該參數。如果你提供了該參數:高優先級執行的早,低優先級的可能會推遲執行。
自定義事件對象:
我們之前使用trigger()觸發事件,在這同時我們也創建了事件。但trigger()的參數有限,我們只能指定事件的對象,參數,名稱。實際上我們可以創建一個自定義事件,在Zendframework裡面有個很重要的事件:MvcEvent。很顯然MvcEvent便是一個自定義事件,該事件組合了application實例,路由器,路由匹配對象,請求和應答對象,視圖模型還有結果。我們查看MvcEvent的源碼會發現MvcEvent類實際上繼承了Event類。同理我們的自定義事件對象也可以繼承Event類或者繼承MvcEvent。
$event = new CustomEvent(); $event->setName('foo'); $event->setTarget($this); $event->setSomeKey($value); //injected with event name and target: $events->triggerEvent($event); //Use triggerEventUntil() for criteria-based short-circuiting: $results = $events->triggerEventUntil($callback, $event);
上面的代碼可以看到我們使用自定義事件類創建了一個事件對象,調用相關攔截器為事件對象設置屬性。我們有了事件對象還是用trigger()觸發事件嗎?顯然不是,我們使用triggerEvent($event)方法,該方法接收一個事件對象。而triggerEventUntil有一個回調函數,該回調函數作為是否進行短回路的依據。