使用事件,可以在特定的時點,觸發執行預先設定的一段代碼,事件既是代碼解耦的一種方式,也是設計業務流程的一種模式。現代軟件中,事件無處不在,比如,你發了個微博,觸發了一個事件,導致關注你的人,看到了你新發出來的內容。對于事件而言,有這么幾個要素:
* 這是一個什么事件?一個軟件系統里,有諸多事件,發布新微博是事件,刪除微博也是一種事件。
* 誰觸發了事件?你發的微博,就是你觸發的事件。
* 誰負責監聽這個事件?或者誰能知道這個事件發生了?服務器上處理用戶注冊的模塊,肯定不會收到你發出新微博的事件。
* 事件怎么處理?對于發布新微博的事件,就是通知關注了你的其他用戶。
* 事件相關數據是什么?對于發布新微博事件,包含的數據至少要有新微博的內容,時間等。
## Yii中與事件相關的類[](http://www.digpage.com/event.html#yii "Permalink to this headline")
Yii中,事件是在?yii\base\Component?中引入的,注意,?yii\base\Object?不支持事件。所以,當你需要使用事件時,請從?yii\base\Component?進行繼承。同時,Yii中還有一個與事件緊密相關的yii\base\Event?,他封裝了與事件相關的有關數據,并提供一些功能函數作為輔助:
~~~
class Event extends Object
{
public $name; // 事件名
public $sender; // 事件發布者,通常是調用了 trigger() 的對象或類。
public $handled = false; // 是否終止事件的后續處理
public $data; // 事件相關數據
private static $_events = [];
public static function on($class, $name, $handler, $data = null,
$append = true)
{
// ... ...
// 用于綁定事件handler
}
public static function off($class, $name, $handler = null)
{
// ... ...
// 用于取消事件handler綁定
}
public static function hasHandlers($class, $name)
{
// ... ...
// 用于判斷是否有相應的handler與事件對應
}
public static function trigger($class, $name, $event = null)
{
// ... ...
// 用于觸發事件
}
}
~~~
## 事件handler[](http://www.digpage.com/event.html#handler "Permalink to this headline")
所謂事件handler就是事件處理程序,負責事件觸發后怎么辦的問題。從本質上來講,一個事件handler就是一段PHP代碼,即一個PHP函數。對于一個事件handler,可以是以下的形式提供:
* 一個PHP全局函數的函數名,不帶參數和括號,光禿禿的就一個函數名。如?trim?,注意,不是?trim($str)?也不是?trim()?。
* 一個對象的方法,或一個類的靜態方法。如?$person->sayHello()?可以用為事件handler,但要改寫成以數組的形式,?[$person,?'sayHello']?,而如果是類的靜態方法,那應該是['namespace\to\Person',?'sayHello']?。
* 匿名函數。如?function?($event)?{?...?}
但無論是何種方式提供,一個事件handler必須具有以下形式:
~~~
function ($event) {
// $event 就是前面提到的 yii\base\Event
}
~~~
也就是只有長得像上面這樣的,才可以作為事件handler。
還有一點容易犯錯的地方,就是對于類自己的成員函數,盡管在調用?on()?進行綁定時,看著這個handler是有效的,因此,有的小伙伴就寫成這樣了?$this->on(EVENT_A,?'publicMethod')?,但事實上,這是一個錯誤的寫法。以字符串的形式提供handler,只能是PHP的全局函數。這是由于handler的調用是通過?call_user_func()?來實現的。因此,handler的形式,與?call_user_func()?的要求是一致的。這將在?[事件的觸發](http://www.digpage.com/event.html#id6)?中介紹。
## 事件的綁定與解除[](http://www.digpage.com/event.html#id2 "Permalink to this headline")
### 事件的綁定[](http://www.digpage.com/event.html#id3 "Permalink to this headline")
有了事件handler,還要告訴Yii,這個handler是負責處理哪種事件的。這個過程,就是事件的綁定, 把事件和事件handler這兩個螞蚱綁在一根繩上,當事件跳起來的時候,就會扯動事件handler啦。
yii\base\Component::on()?就是用來綁定的,很容易就猜到,?yii\base\Component::off()?就是用來解除的。對于綁定,有以下形式:
~~~
$person = new Person;
// 使用PHP全局函數作為handler來進行綁定
$person->on(Person::EVENT_GREET, 'person_say_hello');
// 使用對象$obj的成員函數say_hello來進行綁定
$person->on(Person::EVENT_GREET, [$obj, 'say_hello']);
// 使用類Greet的靜態成員函數say_hello進行綁定
$person->on(Person::EVENT_GREET, ['app\helper\Greet', 'say_hello']);
// 使用匿名函數
$person->on(Person::EVENT_GREET, function ($event) {
echo 'Hello';
});
~~~
事件的綁定可以像上面這樣在運行時以代碼的形式進行綁定,也可以在配置中進行綁定。 當然,這個配置生效的過程其實也是在運行時的。原理可以參見?[_配置項(Configuration)_](http://www.digpage.com/configuration.html#configuration)?部分的內容。
上面的例子只是簡單的綁定了事件與事件handler,如果有額外的數據傳遞給handler,可以使用yii\base\Component::on()?的第三個參數。這個參數將會寫進?Event?的相關數據字段,即屬性?data。如:
~~~
$person->on(Person::EVENT_GREET, 'person_say_hello', 'Hello World!');
// 'Hello World!' 可以通過 $event訪問。
function person_say_hello($event)
{
echo $event->data; // 將顯示 Hello World!
}
~~~
yii\base\Component?維護了一個handler數組,用來保存綁定的handler:
~~~
// 這個就是handler數組
private _events = [];
// 綁定過程就是將handler寫入_event[]
public function on($name, $handler, $data = null, $append = true)
{
$this->ensureBehaviors();
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
array_unshift($this->_events[$name], [$handler, $data]);
}
}
~~~
### 保存handler的數據結構[](http://www.digpage.com/event.html#id4 "Permalink to this headline")
從上面代碼我們可以了解兩個方向的內容,一是?$_event[]?的數據結構,二是綁定handler的邏輯。
從handler數組?$_evnet[]?的結構看,首先他是一個數組,保存了該Component的所有事件handler。 該數組的下標為事件名,數組元素是形為一系列?[$handler,?$data]?的數組,如?[_$_event[]數組的數據結構示意圖_](http://www.digpage.com/event.html#img-event)?所示。
![$_event[]數組的數據結構示意圖](http://www.digpage.com/_images/event.png)
$_event[]數組的數據結構示意圖
在事件的綁定邏輯上,按照以下順序:
* 參數?$append?是否為?true?。為?true?表示所要綁定的事件handler要放在?$_event[]?數組的最后面。這也是默認的綁定方式。
* 參數?$append?是否為?false?。表示handler要放在數組的最前面。這個時候,要多進行一次判定。
* 如果所有綁定的事件還沒有已經綁定好的handler,也就是說,將要綁定的handler是第一個,那么無論?$append?是否是?true?,該handler必然是第一個元素,也是最后一個元素。
* 如果?$append?為?false?,且要綁定的事件已經有了handler,那么,就將新綁定的事件插入到數組的最前面。
handler在?$event[]?數組中的位置很重要,代表的是執行的先后順序。這個在?[多個事件handler的順序](http://www.digpage.com/event.html#id7)中會講到。
### 事件的解除[](http://www.digpage.com/event.html#id5 "Permalink to this headline")
在解除時,就是使用?unset()?函數,處理?$_event[]?數組的相應元素。?yii\base\Component::off()如下所示:
~~~
public function off($name, $handler = null)
{
$this->ensureBehaviors();
if (empty($this->_events[$name])) {
return false;
}
// $handler === null 時解除所有的handler
if ($handler === null) {
unset($this->_events[$name]);
return true;
} else {
$removed = false;
// 遍歷所有的 $handler
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
}
return $removed;
}
}
~~~
要留意以下幾點:
* 當?$handler?為?null?時,表示解除?$name?事件的所有handler。
* 在解除?$handler?時,將會解除所有的這個事件下的?$handler?。雖然一個handler多次綁定在同一事件上的情況不多見,但這并不是沒有,也不是沒有意義的事情。在特定的情況下,確實有一個handler多次綁定在同一事件上。因此在解除時,所有的?$handler?都會被解除。而且沒有辦法只解除其中的一兩個。
## 事件的觸發[](http://www.digpage.com/event.html#id6 "Permalink to this headline")
事件的處理程序handler有了,事件與事件handler關聯好了,那么只要事件觸發了,handler就會按照設計的路子走。事件的觸發,需要調用?yii\base\Component::trigger()
~~~
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();
if (!empty($this->_events[$name])) {
if ($event === null) {
$event = new Event;
}
if ($event->sender === null) {
$event->sender = $this;
}
$event->handled = false;
$event->name = $name;
// 遍歷handler數組,并依次調用
foreach ($this->_events[$name] as $handler) {
$event->data = $handler[1];
// 使用PHP的call_user_func調用handler
call_user_func($handler[0], $event);
// 如果在某一handler中,將$evnet->handled 設為true,
// 就不再調用后續的handler
if ($event->handled) {
return;
}
}
}
Event::trigger($this, $name, $event); // 觸發類一級的事件
}
~~~
以?yii\base\Application?為例,他定義了兩個事件,?EVENT_BEFORE_REQUEST?EVENT_AFTER_REQUEST?分別在處理請求的前后觸發:
~~~
abstract class Application extends Module
{
// 定義了兩個事件
const EVENT_BEFORE_REQUEST = 'beforeRequest';
const EVENT_AFTER_REQUEST = 'afterRequest';
public function run()
{
try {
$this->state = self::STATE_BEFORE_REQUEST;
// 先觸發EVENT_BEFORE_REQUEST
$this->trigger(self::EVENT_BEFORE_REQUEST);
$this->state = self::STATE_HANDLING_REQUEST;
// 處理Request
$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
// 處理完畢后觸發EVENT_AFTER_REQUEST
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
$this->state = self::STATE_END;
return $response->exitStatus;
} catch (ExitException $e) {
$this->end($e->statusCode, isset($response) ? $response : null);
return $e->statusCode;
}
}
}
~~~
上面的代碼,不用全部去讀懂。只要注意是怎么定義事件,怎么觸發事件的就可以了。
對于事件的定義,提倡使用const 常量的形式,可以避免寫錯。?trigger('Hello')?和trigger('hello')?可是不同的事件哦。原因在于handler數組下標,就是事件名。 而PHP里數組下標是區分大小寫的。所以,用類常量的方式,可以避免這種頭疼的問題。
在觸發事件時,可以把與事件相關的數據傳遞給所有的handler。比如,發布新微博事件:
~~~
// 定義事件的關聯數據
class MsgEvent extend yii\base\Event
{
public $dateTime; // 微博發出的時間
public $author; // 微博的作者
public $content; // 微博的內容
}
// 在發布新的微博時,準備好要傳遞給handler的數據
$event = new MsgEvent;
$event->title = $title;
$event->author = $auhtor;
// 觸發事件
$msg->trigger(Msg::EVENT_NEW_MESSAGE, $event);
~~~
注意這里數據的傳入,與使用?on()?綁定handler時傳入數據方法的不同。在?on()?中,使用一個簡單變量,傳入,并在handler中通過?$event->data?進行訪問。這個是在綁定時確定的數據。而有的數據是沒辦法在綁定時確定的,如發出微博的時間。這個時候,就需要在觸發事件時提供其他的數據了。也就是上面這段代碼使用的方法了。這兩種方法,一種用于提供綁定時的相關數據,一種用于提供事件觸發時的數據,各有所長,互相補充。你可要一碗水端平,不要厚此薄彼了。
## 多個事件handler的順序[](http://www.digpage.com/event.html#id7 "Permalink to this headline")
使用?yii\base\Component::on()?可以為各種事件綁定handler,也可以為同一事件綁定多個handler。假如,你是微博系統的技術人員,剛開始的時候,你指定新發微博的事件handler就是通知關注者有新的內容發布了。現在,你不光要保留這個功能,你還要通知微博中@到的所有人。這個時候,一種做法是直接在原來的handler末尾加上新的代碼,以處理這個新的需要。另一個方法,就是再寫一個handler,并綁定到這個事件上。從易于維護的角度來講,第二種方法是比較合理的。前一種方法由于修改了原來正常使用的代碼,可能會影響原來的正常功能。同時,如果一直有新的需求,那么很快這個handler就會變得很雜,很大。所以,建議使用第二種方法。
Yii中是支持這種一對多的綁定的。那么,在一個事件觸發時,哪個handler會被先執行呢?各handler之間總有一個先后問題吧。這個可能不同的編程語言、不同的框架有不同的實現方式。有的語言是以堆棧的形式來保存handler,可能會以后綁定上去的事件先執行的方式運作。這種方式的好處是編碼的人權限大些,可以對事件進行更改、攔截、中止,移花接木、偷天換日、無中生有,各種欺騙后面的handler。而Yii是使用數組來保存handler的,并按順序執行這些handler。這意味著一般框架上預設的handler會先執行。但是不要以為Yii的事件handler就沒辦法偷天換日了,要使后加上的事件handler先運行,只需在調用?yii\base\Component::on()?進行綁定時,將第四個參數設為?$append?設為?false那么這個handler就會被放在數組的最前面了,它就會被最先執行,它也就有可能欺騙后面的handler了。
為了加強安全生產,國家安監局對某個煤礦進行監管,一旦發生礦難,他們會收到報警,這就是一個事件和一個handler:
~~~
$coal->on(Coal::EVENT_DISASTER, [$government, 'onDisaster']);
class Government extend yii\base\Component
{
... ...
public function onDisaster($event)
{
echo 'DISASTER! from ' . $event->sender;
}
}
~~~
由于煤礦自身也要進行管理,所以,政府允許煤礦可以編寫自己的handler對礦難進行處理。 但是,這個小煤窯的老板,你有張良計,我有過墻梯,對于發生礦難這一事件編寫了一個handler專門用于瞞報:
~~~
// 第四個參數設為false,使得該handler在整個handler數組中處于第一個
$coal->on(Coal::EVENT_DISASTER, [$baddy, 'onDisaster'], null, false);
calss Baddy extend yii\base\Component
{
... ...
public function onDisaster($event)
{
// 將事件標記為已經處理完畢,阻止后續事件handler介入。
$event->handled = true;
}
}
~~~
壞人不可怕,會編程的壞人才可怕。我們要阻止他,所以要把綁定好的handler解除。這個解除是綁定的逆向過程,在實質上,就是把對應的handler從handler數組中刪除。使用?yii\base\Component::off()就能刪除:
~~~
// 刪除所有EVENT_DISASTER事件的handler
$coal->off(Coal::EVENT_DISASTER);
// 刪除一個PHP全局函數的handler
$coal->off(Coal::EVENT_DISASTER, 'global_onDisaster');
// 刪除一個對象的成員函數的handler
$coal->off(Coal::EVENT_DISASTER, [$baddy, 'onDisaster']);
// 刪除一個類的靜態成員函數的handler
$coal->off(Coal::EVENT_DISASTER, ['path\to\Baddy', 'static_onDisaster']);
// 刪除一個匿名函數的handler
$coal->off(Coal::EVENT_DISASTER, $anonymousFunction);
~~~
其中,第三種方法就可以把小煤窯老板的handler解除下來。
細心的讀者朋友可能留意到,在刪除匿名函數handler時,需要使用一個變量。請讀者朋友留意,就算你調用?yii\base\Component::on()?yii\base\Component::off()?時,寫了兩個一模一樣的匿名函數,你也沒辦法把你前面的匿名handler解除。從本質上來講,兩個匿名函數就是兩個不同的存在,為了能夠正確解除,需要先把匿名handler保存成一個變量,如上面的?$anonymousFunction?,然后再依次綁定、解除。但是,使用了變量后,就失去了匿名函數的一大心理上的優勢,你本不用去關心他的,我的建議是在這種情況下,就不要使用匿名函數了。因此,在作為handler時,要慎重使用匿名函數。只有在確定不需要解除時,才可以使用。
## 事件的級別[](http://www.digpage.com/event.html#id8 "Permalink to this headline")
前面的事件,都是針對類的實例而言的,也就是事件的觸發、處理全部都在實例范圍內。這種級別的事件用情專一,不與類的其他實例發生關系,也不與其他類、其他實例發生關系。除了實例級別的事件外,還有類級別的事件。對于Yii,由于Application是一個單例,所有的代碼都可以訪問這個單例。因此,有一個特殊級別的事件,全局事件。但是,本質上,他只是一個實例級別的事件。
這就好比是公司里的不同階層。底層的碼農們只能自己發發牢騷,個人的喜怒哀樂只發生在自己身上,影響不了其他人。而團隊負責人如果心情不好,整個團隊的所有成員今天都要戰戰兢兢,慎言慎行。到了公司老總那里,他今天不爽,哪個不長眼的敢上去觸霉頭?事件也是這樣的,不同層次的事件,決定了他影響到的范圍。
### 類級別事件[](http://www.digpage.com/event.html#id9 "Permalink to this headline")
先講講類級別的事件。類級別事件用于響應所有類實例的事件。比如,工頭需要了解所有工人的下班時間, 那么,對于數百個工人,即數百個Worker實例,工頭難道要一個一個去綁定自己的handler么? 這也太低級了吧?其實,他只需要綁定一個handler到Worker類,這樣每個工人下班時,他都能知道了。 與實例級別的事件不同,類級別事件的綁定需要使用?yii\base\Event::on()
~~~
Event::on(
Worker::className(), // 第一個參數表示事件發生的類
Worker::EVENT_OFF_DUTY, // 第二個參數表示是什么事件
function ($event) { // 對事件的處理
echo $event->sender . ' 下班了';
}
);
~~~
這樣,每個工人下班時,會觸發自己的事件處理函數,比如去打卡。之后,會觸發類級別事件。 類級別事件的觸發仍然是在?yii\base\Component::trigger()?中,還記得該函數的最后一個語句么:
~~~
Event::trigger($this, $name, $event); // 觸發類一級的事件
~~~
這個語句就觸發了類級別的事件。類級別事件,總是在實例事件后觸發。既然觸發時機靠后,那么如果有一天你要早退又不想老板知道,你就可以向小煤窯老板那樣,通過?$event->handled?=?true?,來終止事件處理。
從?yii\base\Event::trigger()?的參數列表來看,比?yii\base\Component::trigger()?多了一個參數$class?表示這是哪個類的事件。因此,在保存?$_event[]?數組上,?yii\base\Event?也比yii\base\Component?要多一個維度:
~~~
// Component中的$_event[] 數組
$_event[$eventName][] = [$handler, $data];
// Event中的$_event[] 數組
$_event[$eventName][$calssName][] = [$handler, $data];
~~~
那么,反過來的話,低級別的handler可以在高級別事件發生時發生作用么?這當然也是不行的。由于類級別事件不與任意的實例相關聯,所以,類級別事件觸發時,類的實例可能都還沒有呢,怎么可能進行處理呢?
類級別事件的觸發,應使用?yii\base\Event::trigger()?。這個函數不會觸發實例級別的事件。值得注意的是,?$event->sender?在實例級別事件中,?$event->sender?指向觸發事件的實例,而在類級別事件中, 指向的是類名。在?yii\base\Event::trigger()?代碼中,有:
~~~
if (is_object($class)) { // $class 是trigger()的第一個參數,表示類名
if ($event->sender === null) {
$event->sender = $class;
}
$class = get_class($class); // 傳入的是一個實例,則以類名替換之
} else {
$class = ltrim($class, '\\');
}
~~~
這段代碼會對?$evnet->sender?進行設置,如果傳入的時候,已經指定了他的值,那么這個值會保留,否則,就會替換成類名。
對于類級別事件,有一個要格外注意的地方,就是他不光會觸發自身這個類的事件,這個類的所有祖先類的同一事件也會被觸發。但是,自身類事件與所有祖先類的事件,視為同一級別:
~~~
// 最外面的循環遍歷所有祖先類
do {
if (!empty(self::$_events[$name][$class])) {
foreach (self::$_events[$name][$class] as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// 所有的事件都是同一級別,可以隨時終止
if ($event->handled) {
return;
}
}
}
} while (($class = get_parent_class($class)) !== false);
~~~
上面的嵌套循環的深度,或者叫時間復雜度,受兩個方面影響,一是類繼承結構的深度,二是$_event[$name][$class][]?數組的元素個數,即已經綁定的handler的數量。從實踐經驗看,一般軟件工程繼承深度超過十層的就很少見,而事件綁定上,同一事件的綁定handler超過十幾個也比較少見。因此,上面的嵌套循環運算數量級大約在100~1000之間,這是可以接受的。
但是,從機制上來講,由于類級別事件會被類自身、類的實例、后代類、后代類實例所觸發,所以,對于越底層的類而言,其類事件的影響范圍就越大。因此,在使用類事件上要注意,盡可能往后代類安排,以控制好影響范圍,盡可能不在基礎類上安排類事件。
### 全局事件[](http://www.digpage.com/event.html#id10 "Permalink to this headline")
接下來再講講全局級別事件。上面提到過,所謂的全局事件,本質上只是一個實例事件罷了。他只是利用了Application實例在整個應用的生命周期中全局可訪問的特性,來實現這個全局事件的。當然,你也可以將他綁定在任意全局可訪問的的Component上。
全局事件一個最大優勢在于:在任意需要的時候,都可以觸發全局事件,也可以在任意必要的時候綁定,或解除一個事件:
~~~
Yii::$app->on('bar', function ($event) {
echo get_class($event->sender); // 顯示當前觸發事件的對象的類名稱
});
Yii::$app->trigger('bar', new Event(['sender' => $this]));
~~~
上面的?Yii::$app->on()?可以在任何地方調用,就可以完成事件的綁定。而?Yii::$app->trigger()?只要在綁定之后的任何時候調用就OK了。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者