[TOC]
# 簡介
可以將自定義代碼“注入”到現有代碼中的特定執行點。附加自定義代碼到某個事件,當這個事件被觸發時,這些代碼就會自動執行。
例如,郵件程序對象成功發出消息時可觸發?`messageSend`?事件。如想追蹤成功發送的消息,可以附加相應追蹤代碼到?`messageSend`?事件。
Yii 引入了名為`yii\base\Component`的基類以支持事件。如果一個類需要觸發事件就應該繼承`yii\base\Component`或其子類。
# 事件處理器
* 對象名和方法名數組形式指定的對象方法,如?`[$object, $method]`?
~~~
use app\events\MessageEvent;
...
const EVENT_HELLO = 'sayHello';
public function actionIndex()
{
$event = new MessageEvent;
$event->message = 'new message from actionIndex method';
$this->on(self::EVENT_HELLO,[$event,'hello']);
$this->trigger(self::EVENT_HELLO,$event); // 打印 new message from actionIndex method
}
~~~
其中`app\events\MessageEvent`類代碼很簡單,如下:
~~~
<?php
namespace app\events;
use yii\base\Event;
use Yii;
class MessageEvent extends Event
{
public $message;
public function hello($event)
{
echo $event->message;
}
}
~~~
* 類名和方法名數組形式指定的**靜態類方法**,如?`[$class, $method]`
~~~
use app\events\MessageEvent;
...
const EVENT_HELLO = 'sayHello';
public function actionIndex()
{
$event = new MessageEvent;
$event->message = 'new message from actionIndex method';
$this->on(self::EVENT_HELLO,['app\events\MessageEvent','hello']);
$this->trigger(self::EVENT_HELLO,$event);
}
~~~
其中`app\events\MessageEvent`類代碼很簡單,如下:
~~~
<?php
namespace app\events;
use yii\base\Event;
use Yii;
class MessageEvent extends Event
{
public $message;
static public function hello($event)
{
echo $event->message;
}
}
~~~
* 匿名函數,如?`function ($event) { ... }`
~~~
const EVENT_HELLO = 'sayHello';
public function actionIndex()
{
$this->on(self::EVENT_HELLO,function ($event){ // 綁定一個Event事件
foreach ($event->data as $data){
echo strtoupper($data),PHP_EOL;
}
}, ['abc','dev']);
$this->trigger(self::EVENT_HELLO); // 觸發事件 得到結果:ABC DEV
}
~~~
# 綁定與解除
有了事件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';
});
~~~
`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]);
}
}
~~~
在事件的綁定邏輯上,按照以下順序:
* 參數$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)中會講到。
在解除時,就是使用 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 都會被解除。而且沒有辦法只解除其中的一兩個。
# 事件的級別
## 類級別事件
**當對象觸發事件時,它首先調用實例級別的處理器,然后才會調用類級別處理器**
先講講類級別的事件。類級別事件用于響應所有類實例的事件。比如,工頭需要了解所有工人的下班時間, 那么,對于數百個工人,即數百個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之間,這是可以接受的。
但是,從機制上來講,由于類級別事件會被類自身、類的實例、后代類、后代類實例所觸發,所以,對于越底層的類而言,其類事件的影響范圍就越大。因此,在使用類事件上要注意,盡可能往后代類安排,以控制好影響范圍,盡可能不在基礎類上安排類事件。
## 全局事件
接下來再講講全局級別事件。上面提到過,所謂的全局事件,本質上只是一個實例事件罷了。他只是利用了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了。
- 目錄
- 配置
- 簡介
- 別名
- gii
- 配置項
- 模型
- 簡介
- 增刪改查
- AR和model
- 模型事件
- 場景
- query查詢
- 增刪改
- AR查詢器
- 模型關系定義
- AR模型連表查詢
- fields
- where拼接
- 模塊
- 創建模塊
- 控制器
- 表單
- 跳轉
- 響應
- 驗證器
- Action
- 組件
- url
- 分頁
- 驗證碼
- 緩存
- 文件上傳
- 預啟動組件
- 事件
- 自定義組件
- redis
- 日志
- 行為
- cookie和session
- 基礎知識
- 創建一個類
- 配置一個類
- object基類
- component組件類特性
- phpstorm無法更改php等級
- url地址美化
- 過濾器
- 請求處理
- 請求組件
- 響應組件
- header
- 用戶登錄
- 實現IdentityInterface接口
- 登錄
- 自動檢測登錄
- 獲取用戶信息
- 訪問行為追蹤
- phpstorm+postman斷點調試