提及事件(event),咱們但是一點都不陌生。現實生活當中的事件無處不在,好比你發了一條微博,觸發了一條事件,致使關注你的人收到了一條消息,看到你發的內容;好比你經過支付寶買東西,付了款,觸發一個事件,致使你收到一條短信,告訴你剛剛扣款了,你帳戶餘額還有多少...php
咱們將事件稍稍加以抽象,發現事件具備某些共同特色,好比事件其實不是孤立存在,它只是某個流程或者工序的一個特殊的「點」,能夠理解爲時間點,也能夠理解爲邏輯的點;其次,事件上能夠綁定一些「動做」,好比發送一條短信或者發送一個郵件;第三,我能夠綁定,固然也能夠解綁,若是我反感頻頻的短信提醒,我能夠選擇不發短信,我本身去查看帳戶餘額;第四,這些動做和主流程每每並無直接的關係,每每是「附加」的:我已經付完款了,你發短信或者不發,發郵件或者直接客服通知我其實影響不大,並不影響我購物這個行爲自己——反正我已經付完款,預期不久就會收到商品了。css
其實,說到這裏,已經有點入戲的感受了。人有生老病死,一年有春夏秋冬四季演替,封建王朝有興盛、停滯、衰亡的週期律——「其興也勃焉,其亡也忽焉」。換句話說,人,季節,王朝等等這些世間萬物都有本身的生命週期。nginx
那麼在軟件行業,一個系統,一個組件,一個功能,一個類都是有本身的生命週期的——建立、運行、銷燬。好比一個類(Class)都要通過__construct()
,調用各個類方法,__destruct() 的過程。每一個程序的運行,要理解爲一個過程或者流程。那麼這樣來理解事件就有意義了:事件無非就是這個過程之中一些有意義的「點」。這些點是人爲作的設定,好比插入數據庫數據,那麼校驗前、後,插入前、後就多是幾個有意義的時間節點,把這些節點當作一個個的事件,就更加便於咱們去理解這整個過程。複雜的東西理解起來困難,咱們分紅若干個階段來理解豈不是就簡單許多了嗎?想一想爲啥計算機網絡爲啥要分紅物理層、數據鏈路層、網絡層,運輸層、應用層這個簡單道理就好了。除了咱們更好的去理解程序的運行流程,更爲重要的是還可以使咱們可以「介入」這個流程,改變這個流程,從而實現咱們的目的。這就是往事件上「附加」一些動做或者行爲了,專業點說,就是事件處理器或者事件監聽者。咱們想要在某個特定的時間點作點什麼,就事先在這個對應的事件上綁定事件處理器,當流程走到這一步時,相應的處理器就被執行,完成咱們事先設定的目的。想象一下:軟件的運行就是沿着本身設定的路線,走過一個又一個重要節點,同時觸發這些節點事件,最終走到本身生命終點的一個過程。將事件理解爲流程中的節點,不只能夠幫我更好的認識程序,也能更好的幫助咱們改造程序。數據庫
事件的實現,實際上是觀察者模式的一種體現。可能有人會說,爲啥是觀察者模式?Yii的事件並不符合觀察者模式的經典定義啊?的確,Yii的事件實現機制中確實不存在經典Observer和Subject即觀察者和被觀察者。可是,不要在乎這些細節——觀察者模式主要面臨場景是一對多鬆耦合的對象之間的關係,要解決的問題就是對象狀態改變,其餘對象能獲得通知。「一對多」和「鬆耦合」是其核心要義和功能。看看Yii的事件其實正好能實現這兩個功能:Yii的每一個事件(beforeSave,afterSave這些)都是一個被觀察者,每一個事件處理器都是它的觀察者,被觀察者被觸發時,全部觀察者都依次要執行。編程
小編心得:觀察者模式的經典定義——觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的全部依賴者都會受到通知並自動更新。數組
下面,咱們來談談Yii2自己是如何實現事件的。網絡
在Yii中,事件是Component引入的,BaseObject 是不支持事件的,BaseObject只支持屬性。當你須要使用事件時,就須要繼承Component或者它的子類,而不能直接繼承BaseObject。好在咱們以前反覆提過,Yii2是基於Component的,就是說,Yii2中大部分類都是Component的子類,是天生帶有事件功能的。和其餘框架相比,我覺這是Yii2的一大優點——從「原子」層面都已經讓類具有很強的擴展性了。面對具體業務場景,事件每每成爲一把利劍,使得使用者駕輕就熟。同時,\yii\base\Event是於Yii2事件功能緊密相連的一個類,他封裝與事件相關的有關數據,並能夠自定義一些功能函數做爲輔助。數據結構
class Event extends BaseObject { // 事件的名稱 public $name; // 事件發佈者,一般是調用了 trigger() 的對象或類,也可傳遞別的對象。 public $sender; // 此事件是否被處理了,若爲true,那事件的處理到此爲止 public $handled = false; // 事件的相關數據 public $data; // 保存全局事件的處理器 private static $_events = []; // 綁定類級別事件handler public static function on($class, $name, $handler, $data = null, $append = true) { // ... } // 取消類級別事件handler的綁定 public static function off($class, $name, $handler = null) { // ... } // 解綁全部類級別事件的handler public static function offAll() { self::$_events = []; } // 判斷一個類和其全部父類上是否綁定有事件處理器 public static function hasHandlers($class, $name) { // ... } // 觸發類級別的事件,執行全部這個事件上綁定的handler public static function trigger($class, $name, $event = null) { // ... } }
所謂事件處理器(事件handler)就是事件處理程序。「觸發事件」和執行「事件處理器」實際上是一個意思。說到底,事件處理器究竟是什麼呢?其實僅僅只是一個callable——或者是全局函數,或者是類的方法等。本質上說,事件處理器就是一段PHP代碼,一個PHP函數。app
具體說來,handler 有這麼幾種形式:框架
function ($event) { ... }
$object->handleClick() 即 [$object, 'handleClick']
Page::handleClick()
不管是哪一種形式的handler,它們最終都必需要有以下的形式:
function ($event){...}
這裏的$event正是yii\base\Event類或者其子類的實例。
有了事件處理器,就能夠將其綁定到特定的事件上了。綁定事件處理器是經過yii\base\Component::on() 來進行,相應的解綁是yii\base\Component::off()。
// 綁定的過程就是將handler寫入$_event[]的過程 public function on($name, $handler, $data = null, $append = true) { $this->ensureBehaviors(); //$append爲true時$handler置於數組最後,觸發時最後被執行 if ($append || empty($this->_events[$name])) { $this->_events[$name][] = [$handler, $data]; } else { //$append 爲false時$handler置於數組最前,觸發時最早被執行 array_unshift($this->_events[$name], [$handler, $data]); } }
例如:
$order = new Order(); $order->on(Order::EVENT_CREATING, [$obj, 'verifyOrder',false]) ; $order->on(Order::EVENT_CREATED, 'checkOrder') ; $order->on(Order::EVENT_PAYED, ['\common\lib\Email', 'send']) ; $order->on(Order::EVENT_PAYED, function ($event) { echo '訂單已經支付'; });
綁定事件處理器時能夠提供額外數據做爲 \yii\base\Component::on() 方法的第三個參數,數據在事件被觸發時能被處理器使用。如:
// 第三個參數傳遞數據,當訂單被支付時輸出'訂單已支付' $order->on(Order::EVENT_PAYED, function ($event) { echo $event->data; }, '訂單已支付');
事件處理器的綁定能夠如上在運行的時候進行,也能夠在配置的進行綁定。不管哪一種方式,都必須是先綁定,後觸發。
public function off($name, $handler = null) { $this->ensureBehaviors(); if (empty($this->_events[$name])) { return false; } // 若是沒有傳遞$handler參數,那麼解綁事件$name名下的全部handler if ($handler === null) { unset($this->_events[$name]); return true; } $removed = false; // 若是傳遞參數$name,那麼解綁$name下特定的handler:$handler foreach ($this->_events[$name] as $i => $event) { // 注意$event的第一個元素是hanlder,第二個是$data if ($event[0] === $handler) { unset($this->_events[$name][$i]); $removed = true; } } if ($removed) { $this->_events[$name] = array_values($this->_events[$name]); } return $removed; }
解綁的例子:
// 解綁事件下全部的事件處理器 $order->off(Order::EVENT_CREATING) ; // 解綁事件下checkOrder處理器 $order->off(Order::EVENT_CREATED, 'checkOrder') ;
通常來講,能夠經過on綁定的handler,均可以經過off來解綁。可是匿名函數是除外的,你不能解綁某個特定的匿名函數,除非你事先將其保存爲變量,然後經過這個變量來指定其解綁。
$myHandler = function($event) {....} $order->on(Order::EVENT_CREATED, $myHandler); $order->off(Order::EVENT_CREATED, $myHandler);
在Component中$_events專門用來維護這些handler:
private $_events = [];
事件的綁定邏輯是這樣的:
$append
是否爲 true , 表示所要綁定的事件handler要放在 $_event[] 數組的最後面。這也是默認的綁定方式。$_event[EVENT_NAME][]
只有這麼一個元素,既是第一個也是最後一個handler在 $event[] 數組中的位置就表明執行的前後順序,在現實生活中每每意義重大。
事件經過Component::trigger()來觸發,觸發本質就是執行事件handler的過程。
$order->trigger(Order::EVENT_CREATED);
源碼以下:
public function trigger($name, Event $event = null) { $this->ensureBehaviors(); if (!empty($this->_events[$name])) { // 執行handler必須傳遞一個Event實例,用來傳遞數據 if ($event === null) { $event = new Event(); } // 指定是誰觸發的這個事件,默認就是trigger調用者自身 if ($event->sender === null) { $event->sender = $this; } $event->handled = false; // 默認事件沒有被處理 $event->name = $name; // 事件名稱 foreach ($this->_events[$name] as $handler) { $event->data = $handler[1]; // 最關鍵的地方:全部handler都是經過call_user_func來執行的 call_user_func($handler[0], $event); // 若是在$handler[0]中,$event->handled被置爲true,表示事件已經被處理好了,後續handler不用再執行了 if ($event->handled) { return; } } } // 執行類級別的事件處理器 Event::trigger($this, $name, $event); }
在觸發事件時,咱們常常會經過Event對象來傳遞數據: 假若有個場景是在文章下面評論送積分,那麼評論後就會觸發送積分的事件user_after_publish,另外咱們知道送積分還有不少別的場合,不一樣場合送的分數不同。所以咱們積分須要有個類PointEvent來表示:
use yii\base\Event; class PointEvent extends Event { //要贈送的積分數量 public $points = 0; // 事件處理結果消息 public $msg = ''; // 其餘方法 }
發表評論以前,先綁定「送積分」事件handler:
// 綁定handler $user = Yii::$app->user->identity; $user->on(User::EVENT_AFTER_PUBLISH, [$obj, 'afterPublish']); .... // 在某個時間點觸發 // 實例化一個PointEvent,points 指定爲 10 $event = new PointEvent(); $event->points = 10; $user->trigger(User::EVENT_AFTER_PUBLISH, $event);
事件處理器要爲發表評論的用戶積分+10:
public function afterPublish($event) { $user = $event->sender; $points = $event->points; $user->points += $points; $user->save(); }
說到這裏,可能有的小夥伴會問,用戶發表評論得到積分這麼個功能爲啥要經過這種方式來實現,爲啥不用下面「簡單」的方式來實現呢:
public function publishComment() { $param = Yii::$app->request->post(); $user = Yii::$app->user->identity; // 新建評論 $comment = new Comment(); $comment->load($param, 'data'); $comment->save(); // 更新用戶積分 $user->points += intval($param['points']); $user->save(); }
這樣作,就是把新建評論和更新用戶放在一個方法裏面。可是這樣作將會有個隱形的問題,若是用戶評論以後,不光要積分+10,還要告知文章做者怎麼辦?這裏只能在publishComment()後面繼續添加代碼了,若是哪天又要發送郵件啥的,還得繼續往裏面添加代碼。這違反了「面對擴展開放,面對修改關閉」的編程原則,長此以往這塊代碼就會變得很是臃腫,很難再維護,分出去再寫幾個方法也無濟於事。
分析這緣由,就是從功能上說,評論和用戶得到積分實際上是鬆耦合的關係——你能夠給積分也能夠不給積分。可是在publishComment()方法你把這兩個功能捆綁的死死的,絲毫分不開——這就是一種糟糕的設計。
相反,用事件就能很好解決這個問題。咱們說,事件實際上是一個流程上的某個特定的點,這裏是流程就是用戶發表評論,EVENT_AFTER_PUBLISH是發表成功後的一個節點,程序走到這裏,觸發一下。有處理器就執行,沒有就繼續日後執行。咱們綁定了一個送積分的處理器 ,也能夠綁定推送消息的處理器。這麼看待問題,就已經解耦了兩種功能。在實現上,將發表評論看作流程自己,而送積分,推送提醒看作是「附加」的,在須要的時候綁定下,不須要就不綁定。
下面是主流程:
public function publishComment() { $user = Yii::$app->user->identity; // 新建以前的時間點:觸發EVENT_BEFORE_PUBLISH $user->trigger(User::EVENT_BEFORE_PUBLISH); // 新建評論 $comment = new Comment(); $comment->load($param, 'data'); $comment->save(); // 新建以後的時間點:觸發下 EVENT_AFTER_PUBLISH $user->trigger(User::EVENT_AFTER_PUBLISH, $event); }
有的場合是須要送積分:
$user->on(User::EVENT_AFTER_PUBLISH, [Points, 'givePoints']);
有的場合是要發郵件提醒:
$user->on(User::EVENT_AFTER_PUBLISH, [Email, 'notifyPoserOwner']);
還有的場合是須要判斷用戶有沒有發表評論的權限:
$user->on(User::EVENT_BEFORE_PUBLISH, [Authorization, 'checkAuth']); ... public function checkAuth($event) { $user = $event->sender; if (!$user->isAdmin) { throw new AccessDeniedException('你沒有權限發表評論'); } }
事件處理器Points::givePoints,Email::notifyPoserOwner,Authorization::checkAuth 分佈在不一樣的類中,並無出如今上面publishComment裏面,並且可增可減,徹底視須要而定。所以真正知足了「開閉原則」的要求。
小編心得:開閉原則——對擴展開放,對修改關閉。這是代碼組織當中的一條很是重要的原則。
前面的事件,都是針對類的實例而言的,也就是事件的觸發、處理所有都在實例範圍內。這種級別的事件用情專注,不與類的其餘實例發生關係,也不與其餘類、其餘實例發生關係。除了實例級別的事件外,還有類級別的事件。
這就比如是公司裏的不一樣階層。底層的碼農們只能本身發發牢騷,我的的喜怒哀樂只發生在本身身上,影響不了其餘人。而團隊負責人若是心情很差,整個團隊的全部成員今天都要戰戰兢兢,慎言慎行。到了公司老總那裏,他今天不爽,那麼公司全部員工均可能跟着遭殃。事件也是這樣的,不一樣層次的事件,決定了他影響到的範圍。
類級別的事件是由\yii\base\Event中的方法來實現的,和實例級別的事件在Component中的實現原理大同小異,只不過做用範圍不一樣:
好比,我打算全部人發表評論後,都要發送給文章做者了。那麼每一個用戶都去綁定一下那是多麼糟糕的事情,那咱們能夠將發送提醒綁定在User類上,那麼任何$user實例在發表評論後的節點上都綁定了發送提醒的處理器了。
若是某個場合又給某個實例$user
綁定了送積分的事件,那麼等評論一發表,用戶首先得到積分,而後收到郵件——實例級別的事件老是先觸發,類級別的事件老是後觸發。別的$user
沒有綁定送積分的話固然就沒有積分送了。
類級別事件handler的綁定經過Event::on()來實現的:
public static function on($class, $name, $handler, $data = null, $append = true) { $class = ltrim($class, '\\'); // 若是$append爲true,那麼$handler置於handler列表最後面 if ($append || empty(self::$_events[$name][$class])) { self::$_events[$name][$class][] = [$handler, $data]; } else { // 若是$append爲true,那麼$handler置於handler列表最前面 array_unshift(self::$_events[$name][$class], [$handler, $data]); } }
相比Component::on(),Event::on()
還須要傳入類(名) $class
。Event維護的$_event[]
要比Component下的$_event[]
多一個維度:$class
。綁定和解綁事件handler其實至關於向$_events[$name][$class]
數組插入/刪除handler的過程。當$append爲true,$handler
將被最後執行(默認狀況),當爲false,$handler
將被第一個執行,除非有後面的handler取代其位置。
Event::on(User::class, Comment::EVENT_AFTER_PUBLISH, [$this, 'afterPublish'], $data); Event::on(User::class, Comment::EVENT_BEFORE_PUBLISH, [$this, 'beforePublish'], [], false);
類級別事件解綁經過Event::off(),和實例級別事件解綁過程相似:
public static function off($class, $name, $handler = null) { $class = ltrim($class, '\\'); if (empty(self::$_events[$name][$class])) { return false; } if ($handler === null) { unset(self::$_events[$name][$class]); return true; } $removed = false; foreach (self::$_events[$name][$class] as $i => $event) { if ($event[0] === $handler) { unset(self::$_events[$name][$class][$i]); $removed = true; } } // ... }
若是傳遞參數$handler
,那麼解綁類$class
下事件$name
中的$handler,不然就將$class
下的事件$name
的處理器清空。
事件的觸發經過Event::trigger()來實現:
public static function trigger($class, $name, $event = null) { if (empty(self::$_events[$name])) { return; } if ($event === null) { $event = new static(); } $event->handled = false; $event->name = $name; if (is_object($class)) { // 當$class爲一個對象時,sender默認便指定爲這個對象 if ($event->sender === null) { $event->sender = $class; } $class = get_class($class); } else { $class = ltrim($class, '\\'); } // 將$class,$class全部的父類,$class全部實現的接口放入待檢查的數組$classes $classes = array_merge( [$class], class_parents($class, true), class_implements($class, true) ); foreach ($classes as $class) { if (empty(self::$_events[$name][$class])) { continue; } // 若是$classes中綁定有事件$name,那麼就執行其名下全部handler foreach (self::$_events[$name][$class] as $handler) { $event->data = $handler[1]; // 類級別的事件handler最終也是經過call_user_func來執行,並傳遞事件$event call_user_func($handler[0], $event); if ($event->handled) { return; } } } }
經過上面的源碼和註釋,可知類級別的事件能夠向全部父類傳遞,子類事件handler執行,父類同名事件的handler也要執行。因爲類級別事件會被類自身、類的實例、後代類、後代類實例所觸發,因此,對於越底層的類而言,其類事件的影響範圍就越大。所以,在使用類事件上要注意,儘量日後代類安排,以控制好影響範圍,儘量不在基礎類上安排類事件。
還有一種更爲抽象方式來處理事件,你能夠爲特殊的事件建立一個獨立的接口, 而後在你須要的類中實現它。好比:
namespace app\interfaces; interface DanceEventInterface { const EVENT_DANCE = 'dance'; }
而後在兩個類中實現:
class Dog extends Component implements DanceEventInterface { public function meetBuddy() { echo "Woof!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } } class Developer extends Component implements DanceEventInterface { public function testsPassed() { echo "Yay!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } }
要處理由這些類觸發的 EVENT_DANCE ,調用 Event::on()來綁定,並將接口類名做爲第一個參數: 你能夠在這些類中觸發這個事件:
// trigger event for Dog class Event::trigger(Dog::class, DanceEventInterface::EVENT_DANCE); // trigger event for Developer class Event::trigger(Developer::class, DanceEventInterface::EVENT_DANCE);
其實這沒有什麼神祕的,看看Event::trigger()中有這麼一段你就明白了:
$classes = array_merge( [$class], class_parents($class, true), // $class 實現的接口事件也觸發下 class_implements($class, true) );
全局事件本質上也是實例事件的一種,只不過是能夠在任何地方進行綁定,解綁,觸發。好比Application和Yii::$app所管理的任何組件。全局事件無非是由於這些實例是可全局訪問的罷了。由於這些實例都是繼承Component的,因此用法和Component的事件基本相同:
use app\components\Foo; Yii::$app->on('beforeRequest', function ($event) { echo get_class($event->sender); // 顯示 "app\components\Foo" }); Yii::$app->trigger('beforeRequest', new Event(['sender' => new Foo]));